Files
Comfyui-Nodes-Stats/docs/plans/2026-06-21-trial-enable-design.md
T
Ethanfel 1a2bd2bcef docs: design for workflow tab + temporary trial-enable
Approved brainstorming design: a Workflow tab that splits a loaded
workflow's unresolved nodes into Missing (defer to Manager) and Disabled
(enable temporarily under a 7 distinct-boot-day rolling trial, or
permanently). Backend tracks trial state and counts boot-days; frontend
drives Manager enable/disable (Approach A).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:58:45 +02:00

7.0 KiB

Design: Workflow tab + temporary trial-enable for disabled packages

Date: 2026-06-21 Status: Approved

Summary

When a loaded workflow references node types that aren't currently resolvable, Node Stats shows a Workflow tab that splits them into:

  • Missing — the owning package is not installed (or unknown). Handled by ComfyUI Manager as usual (install).
  • Disabled — the owning package is installed but disabled (in custom_nodes/.disabled). Offers two actions:
    • Enable temporarily — re-enable the package under a rolling 7-day trial.
    • Enable permanently — normal re-enable, never auto-disabled.

A temporarily-enabled package auto-disables again if it goes 7 distinct boot-days without being used in an executed prompt. Any execution use resets the counter back to 7 (rolling window), so a package stays enabled as long as it keeps getting used.

Decisions (from brainstorming)

  • Graduation model: rolling. Each execution use resets the counter to the full budget (7). 7 distinct boot-days unused → auto-disable. No permanent "graduation"; a trial package stays trial-managed, kept alive by use.
  • "Used" means: the package's node appears in an executed/queued prompt. Reuses the existing usage tracking. Loading a workflow does not count.
  • Granularity: the tab lists individual node types ("list by node"), but every action resolves to and operates on the owning package (Manager works per package). Trial state is tracked per package.
  • Surfacing: on workflow load, if any node is disabled/missing, auto-open the Node Stats dialog to the Workflow tab.
  • Disabled actions: both "Enable temporarily" (trial) and "Enable permanently".
  • Missing: defer to ComfyUI Manager (install), as usual.
  • Orchestration (Approach A): the Python backend only tracks trial state, counts boot-days, resets on use, and flags expiry. The frontend performs all ComfyUI-Manager enable/disable mutations (server is up, reuses the proven disable code, no coupling to Manager internals). Day-counting stays accurate across headless restarts; the actual auto-disable executes the next time the web UI loads.

Data model

New SQLite table trial_packages in the existing DB:

column meaning
package (PK) directory name (matches our package keys)
enabled_at ISO timestamp the trial started
last_use_day YYYY-MM-DD of last execution use (init = enable day)
last_boot_day YYYY-MM-DD of last boot-day counted
unused_boot_days counter, 0..budget
budget distinct boot-days allowed unused (default 7)

Lifecycle

  • Start trial (after frontend temp-enable): upsert with unused_boot_days=0, last_boot_day=today, last_use_day=today, enabled_at=now. The enable day never counts toward expiry ("not the same day").
  • Boot tick (tracker.tick_boot_days(), called once at __init__ import): for each trial row, if last_boot_day != today: unused_boot_days += 1, last_boot_day = today.
  • Expiry: computed, not stored — expired = unused_boot_days >= budget.
  • Use reset (reset_trials_for(packages), called from _record_prompt after record_usage): for each used package on trial, set unused_boot_days = 0, last_use_day = today.
  • Stop trial (stop_trial(package)): delete the row. Called after permanent enable, after the frontend disables an expired pack, or if re-disabled.

Disabling an already-disabled package via Manager is a no-op, so a stale trial row (e.g. package disabled out-of-band) resolves cleanly on next expiry pass.

Backend (Python)

  • tracker.py: add table to SCHEMA; methods start_trial, stop_trial, tick_boot_days, reset_trials_for(packages), get_trials.
  • __init__.py:
    • At import: tracker.tick_boot_days() wrapped in try/except (never blocks extension load).
    • In _record_prompt: after record_usage, map used class_types → packages and call tracker.reset_trials_for(packages).
    • Routes:
      • GET /nodes-stats/trials[{package, unused_boot_days, budget, days_remaining, expired, enabled_at, last_use_day}, ...]
      • POST /nodes-stats/trials/start {package} → upsert trial
      • POST /nodes-stats/trials/stop {package} → delete trial

The backend never calls ComfyUI Manager.

Frontend (JS)

  • WF-load hook: wrap app.loadGraphData (and/or graph-changed event); collect node types present in the graph but not in LiteGraph.registered_node_types (unresolved).
  • Classify unresolved types using:
    • /customnode/getmappings?mode=local → class_type → pack key
    • /customnode/getlist?mode=local&skip_update=true → pack install state
    • pack state disabled → Disabled bucket; not-installed/unmappable → Missing bucket. (Reconcile the getmappings pack key against getlist entries by id/cnr_id/aux_id — verify field names during implementation.)
  • Workflow tab, auto-opened when ≥1 unresolved node:
    • Disabled rows (by node): [Enable 7d] (temp → Manager enable, then trials/start), [Enable] (permanent → Manager enable, then trials/stop if previously on trial). Show "on trial — N day-boots left" when applicable.
    • Missing rows (by node): [Install] → defer to Manager install.
    • Actions resolve to the owning package and dedupe.
  • Expiry execution: on app load / dialog open, GET /nodes-stats/trials; for expired packs, disable via Manager (reuse existing disable code) → trials/stop → toast "auto-disabled N unused trial package(s)".
  • Enable/disable need a restart to apply in the running session → reuse the existing restart banner.

Error handling

  • ComfyUI Manager absent → feature inert (no classification, no actions), same as the existing disable feature.
  • HTTP/DB failures → toast + log; never corrupt trial state. Failed expiry disable keeps the row for a later session (retry).
  • Package disabled out-of-band → expiry disable no-ops; row cleaned on stop.

Testing

  • tests/test_trials.py (pure logic, mocked dates like existing tests):
    • start_trial initializes counters; enable day not counted
    • tick_boot_days increments only on a new calendar day (same-day reboots = 1)
    • reset_trials_for zeroes the counter
    • expiry triggers at unused_boot_days >= budget
    • start/stop/get round-trips
  • Frontend verified manually (graph load → tab → enable/disable → expiry).

Known implementation risk

The enable payload (/manager/queue/install + skip_post_install=true) needs the correct id/version for disabled packs — the same class of detail as the disable-payload bug already fixed. Verify empirically against a live disabled package during implementation rather than assuming.

Files touched

  • tracker.py — trial table + methods
  • __init__.py — boot tick, usage reset hook, 3 routes
  • js/nodes_stats.js — WF-load hook, classification, Workflow tab, actions, expiry execution
  • tests/test_trials.py — backend unit tests
  • README.md, pyproject.toml — docs + version bump