* 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>