* fix(snapshot): prevent drift corruption from duplicate installs + boot-time mismatch (#161, #162) Closes #162 and #161 as the paired fix the issues describe — both touch the same data-loss surface and verify against the same dev-sync rig. installs at import time, log critical, register nothing in the alphabetical loser; client-side `window` sentinel as belt-and-suspenders; align .env.example / dev-iteration-loop / sync_to_dev / README at the Registry-derived `koolook/` path. compare against live /userdata + picks, set a drift flag on mismatch; gate periodic + pre-load autosave routing to `_unsaved_autosave/` while drifted; promote status pill to "drifted" precedence over "saved" with a recovery tooltip; auto-clear on the next markStateSaved (Save / Load). 26 new tests (14 install guard, 4 sidebar sentinel, 8 boot drift) — 316 total passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(snapshot): apply review fixes — canonical fingerprint + early loser exit + drift timestamp Review-driven follow-ups on PR #186: 1. **Symmetrize file-side fingerprint (MEDIUM).** `detectBootDrift` now uses a new `_canonicalStateFingerprint()` that normalizes the workflows tree through `normalizeWorkflowsStore` AND deep-sorts keys via `sortJsonValue`. Without this, hand-edited / cross-machine / pre-v0.3 files would false-positive as drifted on first boot (e.g. `archived: "false"` string vs `false` bool, missing `directories: {}`, key insertion-order asymmetry from the `{...wf, archived, module, tags}` spread). `_computeFingerprint` stays unchanged so the localStorage save baseline contract holds for existing users. New regression test verifies a shape-only diff does NOT trigger drift. 2. **Gate fork imports on `_is_winning_install` (LOW).** Non-winning duplicate installs now short-circuit before the heavy torch / PIL / diffusers / aiohttp imports, exiting in ~5 LoC instead of paying full import cost just to discard the mappings. 3. **Drift timestamp uses `driftDetectedAt`, not stale `lastNamedSaveAt` (LOW).** The localStorage save baseline could be from a prior session whose live state was already corrupt — surfacing it as "Last named save" in the drift tooltip would mislead. Replaced with the session-noticed-at timestamp from `_bootDriftDiagnostics`. 352 tests passing (316 prior + 26 from PR #186 + 1 new normalize- symmetry case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(install-guard): fail safe on unreadable / non-UTF-8 siblings (#162) Re-review follow-up. The import-time duplicate-install scan could crash the entire plugin load (fail-silent — the alphabetical winner vanished too, not just the loser) on two unguarded paths: - detect_duplicate_koolook_installs: the per-entry is_dir()/is_file() over custom_nodes/ siblings was unguarded, so a non-traversable sibling (restrictive perms, broken mount, a special dir) raised PermissionError up into __init__.py. No duplicate even required. Now caught per-entry and skipped. - read_pyproject_version: read_text(encoding="utf-8") raises UnicodeDecodeError (a ValueError, NOT OSError) on a non-UTF-8 / binary pyproject.toml, escaping the `except OSError`. Now degrades to "?" via `except (OSError, ValueError)`. - __init__.py: wrapped the guard call site as a final backstop — any unforeseen guard failure now degrades to register-normally rather than aborting the import. Both paths reproduced before the fix; the first surfaced while running the suite from a /tmp checkout whose parent held a root-owned dir. 3 regression tests added (non-traversable sibling skipped; non-UTF-8 pyproject -> "?"; report survives a non-UTF-8 sibling). Full suite: 452 passed (449 + 3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: kflame99 <261674183+kflame99@users.noreply.github.com>
7.7 KiB
Iterative dev loop — sidebar work
How the Curated Nodes / Workflows sidebar was built, in case you come back to it after a break. Optimized for fast iteration without the publish round-trip.
First-time setup on a new machine
KOLOOK_COMFYUI_DEV_PATH should point at the eventual Koolook subdirectory
inside your live ComfyUI custom_nodes/ folder — not the custom_nodes/
parent itself.
Target the koolook/ leaf, not ComfyUI-Koolook/. The ComfyUI Manager
and the Comfy Registry both install at custom_nodes/koolook/ (derived from
[project].name = "koolook" in our pyproject.toml).
Pointing dev-sync at the same path overwrites the Manager install in place,
which is what you want. Targeting custom_nodes/ComfyUI-Koolook/ instead
creates a parallel install — both get loaded on every ComfyUI boot, both
register the same routes / sidebar tab, both write to the same /userdata
file, and the workflow store silently corrupts on every restart. The
__init__.py duplicate-install guard catches this and prints a critical
log naming both paths, but the cleanest path is to never create the dual
install. See issue #162
for the full failure mode.
| Platform | Typical path |
|---|---|
| macOS | /Volumes/Data/ComfyUI/custom_nodes/koolook |
| Linux | /home/<user>/ComfyUI/custom_nodes/koolook |
| Windows | C:/ComfyUI_portable/ComfyUI/custom_nodes/koolook (forward slashes work fine) |
cp .env.example .env # if you don't have one yet
$EDITOR .env # set KOLOOK_COMFYUI_DEV_PATH
python scripts/sync_to_dev.py --init # creates the leaf folder + first sync
# restart ComfyUI to pick up the new install
After that first run, drop the --init — plain python scripts/sync_to_dev.py
is enough for every subsequent edit. The --init flag is a one-shot guard
against typos: it refuses to create a Koolook folder unless the parent looks
like a ComfyUI custom_nodes/ directory (named custom_nodes or sitting
inside a ComfyUI parent).
The loop
- Edit
web/koolook_sidebar.jsor any module underweb/sidebar/(or related JSON) in the worktree. python scripts/sync_to_dev.py— copies runtime files into your live ComfyUIcustom_nodes/<koolook>/. ReadsKOLOOK_COMFYUI_DEV_PATHfrom.env.- Restart ComfyUI; verify visually.
- If a design mockup exists for what you just implemented (look
under
docs/designs/), screenshot the running result and diff against the mockup. Don't request review until visual parity is reached or each deviation is documented. See the project CLAUDE.md "Visual verification for design-driven implementation" section for the full rule. - Iterate.
End-to-end: ~10–30 seconds. No tag/publish round-trip.
Trigger phrases when working with the agent
| You say | Agent does |
|---|---|
dev-sync (or "sync to dev", "copy those files") |
runs scripts/sync_to_dev.py |
go |
implements the most-recently proposed plan as code |
| paste exported JSON from the ↓ button | replaces the matching web/<feature>_defaults.json |
What survives across re-syncs and ComfyUI restarts
| Data | Lives in | Persistence |
|---|---|---|
| Node favorites (per user) | localStorage["koolook.curated.userPicks.v1"] |
survives JS updates, ComfyUI restarts, browser refreshes |
| Saved workflows incl. archive (per install) | ComfyUI /userdata/koolook_workflows.json |
survives JS updates and ComfyUI restarts |
| Distributed defaults — starter preset | web/starter_preset.json (in repo) |
copied once into the user's snapshot library as starter.json on first ComfyUI load (replaces the legacy curated_defaults.json localStorage seed) |
| Distributed defaults — workflows | web/workflow_defaults.json (in repo) |
seeded once into /userdata on first ComfyUI load |
dev-sync and manual ComfyUI restarts never wipe user state. Only
localStorage.clear(), deleting /userdata/koolook_workflows.json, or
wiping browser data does.
Commit / push / publish gates
- Commit after a stable batch of working changes — not after every edit.
- Pushing a feature/PR branch never triggers a Comfy Registry publish.
- Publish fires only when
pyproject.tomlchanges and the push lands onmain. Full procedure:releasing.md.
Tips that paid off in this build
- Annotated screenshots (a red arrow on the spot you're talking about) beat text descriptions for UI feedback every time.
- Verify uncertain frontend APIs against
Comfy-Org/ComfyUI_frontendsource before guessing. E.g. theloadGraphData(graph, true, true, wfName, {})4th-arg trick that names the workflow tab was pulled from upstreamsrc/scripts/app.ts, not guessed. - Defensive fallbacks when calling a newer ComfyUI API surface — multiple property paths inside try/catch so older frontends still work.
- Persist folder expansion state across re-renders so saving doesn't
collapse the user's view (the
pathStatesMap inweb/sidebar/tree.js). - For frontend preview buttons, inspect the virtual-node implementation before guessing. ComfyUI custom nodes such as EasyUse GET/SET can be frontend-only tunnels: render-time Python receives the resolved value, but a JS preview button may only see the visible GET key unless it follows the tunnel itself. Read the installed custom node JS, then simulate the relevant LiteGraph shape locally before asking the maintainer to test.
Debugging frontend preview buttons
Frontend preview buttons, such as Easy AI Pipeline's Get output file path,
are not the same execution path as the node's Python outputs. The button code
runs in browser JS and often peeks at connected widgets; the real render path
runs through ComfyUI execution and may resolve virtual or dynamic nodes that
the preview cannot see by default.
When a preview disagrees with render-time output:
- Confirm whether the fix is Python, JS, or both. Python changes need a
ComfyUI restart; JS changes need the browser tab to reload the extension
module. Restarting ComfyUI alone does not guarantee an already-open tab has
re-imported
web/*.js. - Verify the served extension file directly, e.g.
http://127.0.0.1:8188/extensions/koolook/ai_pipeline.js, and add a short temporary marker when needed so the loaded frontend version is provable. - Inspect the connected custom node's frontend source in the live
custom_nodes/install. EasyUse GET/SET nodes live incomfyui-easy-use/web_version/v1/js/getset.js. - If the connected node is a virtual tunnel, follow its graph semantics
manually. EasyUse
easy getNodestores the visible key in itsConstantwidget; the actual value lives upstream of the matchingeasy setNodeinput link. - Build a small local JS simulation of the LiteGraph shape
(
GET -> SET -> source) before dev-syncing another guess.
Do not assume the in-browser automation can always read app.graph from the
modern ComfyUI frontend. If the graph object is not exposed, use source
inspection, served-file checks, and small simulations instead.
Files this loop touches most
web/koolook_sidebar.js— sidebar entry (boots the extension; module bodies live inweb/sidebar/)web/starter_preset.json— bundled starter preset (replaces the legacycurated_defaults.jsonpick-only seed)web/workflow_defaults.json— workflow defaultsscripts/sync_to_dev.py— dev sync helperdocs/maintainers/curated-sidebar.md— starter-preset round-trip detail