Files
malkuthro-ComfyUI-Koolook/web
malkuthroandClaude Opus 4.7 8dfb966be0 feat(sidebar): snapshot durability — per-preset autosave subfolders, Quick Save, status indicator, sticky persist toasts (#94)
* feat(sidebar): snapshot durability — pre-load + periodic auto-saves, sticky persist toasts, Save-dialog library path

Preventive measures after a state-loss incident: a Load erased work, with no
visible warning and no recovery path. Four changes:

1. **Save dialog library path.** Both the input modal ("Save as new…") and
   the three-way overwrite dialog now show a tail-truncated breadcrumb
   ("…/user/koolook-presets2/") with the full path in the tooltip — the user
   sees where the save will land before clicking. New
   `formatLibraryPathBreadcrumb` helper + `.koolook-modal-pathline` CSS;
   threaded through `getLibraryInfo` (already wired for Load).

2. **Pre-load auto-save.** Every destructive Snapshot Load writes
   `_autosave_pre_load_<iso>.json` to the library BEFORE `applySnapshot`. On
   auto-save failure the Load is aborted entirely with a sticky critical
   toast — the whole premise is "Load never destroys state without a
   recovery point." Pre-load autosaves don't rotate; every Load is a
   permanent recovery file.

3. **Periodic auto-save.** Every 5 minutes a defensive snapshot is written
   to `_autosave_periodic_<iso>.json` IF state changed (string-equality
   fingerprint on picks+workflows, exportedAt stripped). Skipped when tab
   is hidden. Rotates: keeps last 5. First tick at +30s so load/seed
   settle first. Started from sidebar setup() after registration.

4. **Critical (sticky) toasts.** New `criticalToast(msg, {copyText})`
   variant — no auto-dismiss; optional "Copy details" button hands the
   user unsaved JSON for manual recovery. Wired into:
     * `commit()` fallback-only path — warns "saved to browser-local
       fallback only" once per outage, re-arms when /userdata recovers
     * `persistMutation` full-failure path — replaces auto-dismiss toast,
       includes the unsaved JSON via Copy
     * `loadWorkflowsStore` corrupt-file path — sticky so "your data
       looks lost" can't be missed
     * `loadWorkflowsStore` stale-fallback-detected path — was a 4.5s
       toast, now sticky with Copy of the fallback JSON; turns evening-
       work-in-localStorage recovery into an explicit user action

No API or storage changes outside the snapshot library. Existing user
snapshots untouched. New `_autosave_*` files are valid snapshots and
appear in the Load list (sorted to the top via leading underscore) —
deletable via the regular row × button.

Files: 6 changed, +433 / -10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): snapshot status indicator — current preset + saved/auto-saved/unsaved dot

Replaces the static "Snapshot" label in the action row with a dynamic
status display:
  ● <preset name> · <state>

Dot color encodes status:
  • green  — saved      (live state matches the last named save)
  • blue   — auto-saved (matches the latest periodic auto-save only)
  • orange — unsaved    (diverged from any save / auto-save)
  • grey   — none       (no preset tracked)

Tooltip carries the long-form explanation including the latest auto-save
ISO timestamp.

State machine:
  • markStateSaved() baselines the fingerprint after a successful named
    save (writeAndToast) or successful Load (applySnapshot).
  • Periodic auto-save updates _lastPeriodicFingerprint + _lastPeriodicAt
    AFTER a successful tick — flips "unsaved" to "auto-saved" without a
    mutation event.
  • setCurrentPresetName fires SNAPSHOT_STATUS_CHANGED_EVENT so the
    indicator name refreshes when Load/Save updates the tracker.

Persistence:
  • Saved fingerprint is mirrored to localStorage
    ("koolook.snapshot.savedFingerprint.v1") so the saved/unsaved
    distinction survives page reloads. Without this, every reload would
    show "saved" until the user mutated something — defeating the point.
  • At session start, if the tracker has a name AND no persisted
    fingerprint exists, we baseline once (the "first-ever session" edge).
    Imperfect for "user closed mid-edit" but the alternative (always
    show unsaved on first load) is worse UX.

Fingerprint = JSON.stringify({picks, workflows}). Same shape used by
periodic auto-save's change detection — single source of truth, no
risk of "saved" and "auto-saved" disagreeing about what the state is.

Files: 5 changed, +237 / -21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sidebar): per-preset autosave subfolders + Quick Save button

Reorganizes auto-save storage to keep the user's library tidy and add a
fast-path save action.

NEW LAYOUT:

  <library>/
    Wan video kit.json                 ← user-named save (Load list)
    Wan video kit_autosave/            ← shadow folder (HIDDEN from Load)
      periodic.json                    ← single rolling file, overwritten
      pre_load_<iso>.json × ≤5         ← rotates, permanent per Load
    another preset.json
    _unsaved_autosave/                 ← used when no preset is tracked
      periodic.json
      pre_load_<iso>.json × ≤5

WHY:

The previous layout dropped every auto-save as a flat `_autosave_*.json`
file at the library root. Two problems:
  1. Cluttered the user's Load dialog (sorted to top via leading
     underscore but still visible).
  2. Filesystem view (Total Commander, Finder) showed dozens of these
     files alongside the user's named saves — visually noisy.

The per-preset subfolder layout makes the on-disk structure self-
documenting: "the shadow recovery for X lives at X_autosave/". Hidden
from the Load list at the server (filters subdirs + legacy `_autosave_*`
flat files), so the regular Load flow only enumerates user-named presets.

WHY one rolling `periodic.json` (not timestamped):

The latest periodic auto-save IS the most useful periodic state. Earlier
periodic snapshots are worse copies of approximately the same work.
Trades disk diversity for cleanliness — pre-load files cover the
"recovery from a deliberate destructive action" case independently.

WHY timestamped pre-load files (rotated, keep 5):

Each Load is a deliberate destructive action and deserves a permanent
recovery point. Rotation bounds disk; rare "5 Loads back" recovery loss
is the trade-off for not letting the folder grow unbounded.

QUICK SAVE BUTTON:

New `pi pi-save` icon button between Load and Save. When the user has a
preset loaded/saved and wants to save again on top of the same name,
they don't need the dialog with confirmation. One click, no prompt,
toast confirms ("Quick-saved 'X'."). Disabled (greyed out) when no
preset is tracked — the regular Save button remains the entry point
for naming a new preset.

Refreshes its enabled state on `SNAPSHOT_STATUS_CHANGED_EVENT` so that
loading a preset, saving as new, or clearing the tracker via partial-
load failures all flip the button state correctly.

SERVER (koolook_routes.py):

  • New `_DIRNAME_RE` validator (same charset as filenames, no extension)
  • New `_resolve_target(lib_base, subdir, name)` — symlink-escape check
    grounded at LIBRARY base (not subdir base) so a hostile symlink
    inside an auto-save subfolder can't redirect writes outside.
  • GET / POST / DELETE / HEAD `/koolook/presets/file` accept optional
    `?dir=<subdir>` query param. Server auto-creates the subdir on POST.
  • GET `/koolook/presets/list` accepts optional `?dir=` for autosave-
    subfolder enumeration (used by the JS pruner). Without `dir`, hides
    subdirs AND legacy `_autosave_*` flat files at root so the Load
    dialog only sees user-named presets.

JS (snapshot.js):

  • `writePreset` / `readPreset` / `deletePreset` / `presetExists` /
    `listPresets` accept `{dir}` option, propagated to fileQuery.
  • `_autosaveSubdir()` resolves `<presetname>_autosave` or
    `_unsaved_autosave` from the current preset tracker.
  • `writePreLoadAutosave` writes to subfolder, returns
    `<subdir>/<filename>` token; new `_prunePreLoadAutosaves`.
  • Periodic timer overwrites `<subdir>/periodic.json` (single file)
    instead of rotating timestamped files.
  • Removed the global flat-file `_autosave_*` constants.

JS (tree.js):

  • New Quick Save button between Load and Save in the snapshot row.
  • `refreshQuickSaveDisabled` toggles the button on every status event.

BACKWARDS COMPATIBILITY:

Existing users with `_autosave_*` files at the library root from the
previous layout will see them stop being written to (the new code
writes to subfolders only). The server-side list filter hides them
from the Load dialog so they don't visually pollute, but the files
themselves remain on disk for manual recovery / cleanup.

Files: 3 changed, +277 / -96.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sidebar): address review — recovery autosave UI + subtitle on overwrite-confirm

Two review findings on PR #94:

  [P2] Pre-load autosaves were written into per-preset subfolders but
       the Load dialog only listed root-level user-named presets, so
       recovery was unreachable from the UI.

  [P3] The Save-as-new flow's existing-name → "Overwrite?" confirm
       dialog dropped the library breadcrumb (showConfirmModal lacked
       subtitle support, even though showInputModal and
       showThreeWayDialog had it).

P2 — RECOVERY AUTO-SAVES IN THE LOAD DIALOG:

Added a new server endpoint `/koolook/presets/autosaves/list` that walks
every `*_autosave/` subdir (including `_unsaved_autosave/`) and returns
a flat list of recovery files with their subdir, name, mtime, and size.
Why a dedicated endpoint? The user-facing list MUST hide subdirs to keep
the Load list clean; the recovery path is the explicit "show me autosave
folders" API, separated cleanly so its boundary is grep-able.

JS gets a matching `listAutosaves()` helper that fans out preview reads
to extract displayName + counts (N+1 cost is real but bounded — typical
user has ~10-30 autosave files, paid only when the recovery section is
expanded).

The Load dialog gains a collapsible `<details>` section ("▸ Recovery
auto-saves") below the regular preset list. Lazy-loaded on first expand.
Items are grouped by subdir so users can scan visually for the preset
they want to recover from. Each row shows a kind badge (PRE-LOAD orange
/ PERIODIC blue / OTHER), the snapshot's display name, and the standard
preview meta (pick + workflow counts + exportedAt).

Click → confirm modal → optional pre-recovery autosave (recursive
protection — restoring an autosave is itself a destructive Load) →
applySnapshot. Post-restore, the tracker is set to the original named
preset derived from the subdir (`Foo_autosave` → "Foo"), or null for
`_unsaved_autosave`. State indicator flips to "saved" via markStateSaved.

P3 — SUBTITLE ON CONFIRM MODAL:

`showConfirmModal` gains an optional `subtitle` parameter (string or
HTMLElement), same contract as showInputModal and showThreeWayDialog.
The Save-as-new flow's overwrite-confirm now passes `buildPathSubtitle()`
so the user keeps library context across the input → exists-check →
confirm chain. Important when Settings → libraryPath has been changed
mid-session, or facility users juggle multiple shared libraries.

Files: 5 changed, +355 / -1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:15:10 +02:00
..