* fix(snapshot): atomic preset writes + local-timezone tooltips
Hardens the snapshot library against mid-write corruption and stops
the auto-saved indicator from rendering Central Time on every machine.
`POST /koolook/presets/file` now stages writes to `<file>.json.tmp`
and `os.replace`s the final name into place — same atomic-rename
pattern `_write_settings` already uses. Covers every preset write
path through the same route (named, periodic, pre-load, future
recovery snapshots). The `.tmp` suffix is invisible in Load and
Recovery dialogs because both list endpoints filter to `.json`.
`formatCentralTime` is now `formatLocalTime` with the hardcoded
`timeZone: "America/Chicago"` removed — the browser formats with the
user's system timezone, and `timeZoneName: "short"` renders the
matching abbreviation (PDT / EDT / CET / …).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(snapshot): drop timezone abbreviation from local-time tooltip
Follow-up to b0f17b9. `timeZoneName: "short"` rendered the user's local
timezone abbreviation (`GMT+2`, `CDT`, `PDT`, …) in the auto-saved hover
tooltip, but the abbreviation read as clutter — the user already knows
their own timezone. Tooltip now shows just the wall-clock time, e.g.
`May 7, 2026, 11:43 PM`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(snapshot): recovery section deep-link + save-time visibility
What you actually asked for last round, properly delivered this time.
The Load dialog's Recovery auto-saves section was a list of file names
with no path context, no save time, and no escape hatch to the file
system — so power users could see the autosaves existed but couldn't
inspect them in Finder, sort by mtime, or pick a specific recovery
point. Three additions close that gap:
1. **`📂 Open` button at the top of the Load dialog** opens the snapshot
library folder in the OS file manager. Server route
`POST /koolook/presets/reveal` shells out to `open` (macOS),
`explorer.exe` (Windows), or `xdg-open` (Linux), with path-traversal
grounded at the library base via `_resolve_target`.
2. **`📂 Open` button on each `<preset>_autosave` group header** deep-
links into that specific subfolder, so the user lands directly on
the autosave files for one preset.
3. **Per-row save time** — recovery rows now show
`saved May 7, 2:34 PM` from the file's mtime instead of the
`exported <date>` that hid the time-of-day. The bare filename
replaces the redundant `Periodic auto-save · <subdir> · <iso>`
displayName so the timestamp is visible at narrow modal widths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(snapshot): offline-fallback banner gets Restore + Discard actions
The "Offline workflow data found" critical toast used to be a one-way
street — Copy details + Dismiss, but nothing ever cleared the
localStorage key, so the banner re-fired on every page load forever.
Two recovery actions close the loop:
- **Restore as snapshot** — wraps the offline workflows into a full
snapshot envelope (live picks bundled in so a later Load wouldn't
wipe them) and writes it as `recovered-<iso>.json` into the snapshot
library via the now-atomic `writePreset`. Clears the localStorage
key on success → banner self-extinguishes. The user lands on a
durable, in-UI-visible artifact.
- **Discard offline copy** — `showConfirmModal` confirm-then-clear.
Same end state for the banner; explicit user-acknowledged data loss.
Mechanics:
- `criticalToast` extended to accept `actions: [{label, onClick,
primary?, busyLabel?}]`. Existing callers (`{ copyText }` only) are
unchanged.
- Toast firing moved out of `loadWorkflowsStore()` and into
`koolook_sidebar.js` (entry point) where the action handlers can
reach `writePreset` / `loadUserPicks` / `showConfirmModal` without
creating circular imports.
- `clearOfflineFallback()` exported from `workflows_store.js` for
the localStorage `removeItem` step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(snapshot): unify Load dialog header and Recovery group layout
Three visual-consistency fixes so the library section and each
`<preset>_autosave` group header read like the same UI control instead
of two slightly-different ones.
1. **Saved-time on every row.** `loadPreview` now threads `mtime` from
the listing endpoint into the preview object, and `formatPreviewMeta`
prefers it over the snapshot's self-reported `exportedAt`. Regular
snapshot rows now show `saved May 7, 11:53 PM` — same format the
recovery rows already used. Falls back to `exported <date>` when
mtime isn't available.
2. **Recovery group headers get the full path.** Subdir name on top,
`<library>/<subdir>` underneath — the same CSS pair
(`folder-name` + `folder-path`) the library section uses. Reuses
`renderLibraryLocation` with a new `title` option so the autosave
header skips the "Library folder:" prefix while inheriting all
styling.
3. **Library Open button matches the group Open button.** Was
`koolook-modal-button` (larger), now `koolook-snapshot-row-btn`
(smaller, same as group headers). Same flex options on both rows
(`flex-start` alignment, `space-between` justification, `gap: 8px`).
`formatAutosaveMeta` collapsed back into `formatPreviewMeta` since the
formats converged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(snapshot): single disclosure arrow + identical row layouts
Two visual fixes after the previous round still left the Recovery
section feeling different from the Snapshots section.
- Drop the literal `▸` / `▾` characters from the recovery summary —
the native `<details>` element already renders a disclosure
triangle, so the previous text duplicated it. User saw two arrows
side-by-side.
- Wrap each group's rows in `.koolook-snapshot-list` so they get the
same bordered-box treatment around them that the library snapshot
rows have. Strip the outer border + visuals from
`.koolook-recovery-list` (now just a scroll container) and
`.koolook-recovery-group-header` (now just padding) so the inner
classes drive appearance — same `folder-name` + `folder-path` pair
+ `koolook-snapshot-row-btn` Open button + `koolook-snapshot-list`
rows wrapper as the library section. Both surfaces should now read
as the same UI control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(snapshot): single disclosure arrow + identical row layouts"
This reverts commit 82874c3b3ada977c236c03e57479a29126ee68e7.
* Reapply "chore(snapshot): single disclosure arrow + identical row layouts"
This reverts commit 76e52bdf7cce5c6e84e883f8714921e2f506a9b3.
* fix(sidebar): remove backticks from CSS comments inside template literal
The comments I added in 82874c3 contained literal backticks (around
identifiers like `koolook-snapshot-list`) — fine in CSS, FATAL inside
a JS template literal. The CSS string in `ensureStyle()` is a
backtick-delimited template, so an inner backtick prematurely closed
the template, the rest of constants.js parsed as broken JS, the module
failed to load, and every sidebar file that imports from constants.js
collapsed with it. Net effect: entire Kforge Labs sidebar disappeared
in the standalone app.
Why pre-commit `node --check` missed it: that command defaults to
script mode for `.js` files and tolerates this edge case. The browser
loads our files as ES modules (stricter), and would have flagged the
backtick. Verified the fix with `node --check` on a `.mjs` copy of
every sidebar file before committing.
Comments rewritten without backticks. Added an in-CSS-comment warning
calling out the constraint so future changes don't regress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(snapshot): tempfile-based atomic write + onCancel for showConfirmModal
Addresses two review findings on PR #105.
P1: The previous atomic-write used `<file>.json.tmp` — a fixed,
predictable name. An attacker (or accidental user) with write access
to a shared library could plant that name as a symlink pointing
outside the library, and `Path.write_bytes` would FOLLOW the symlink
past the path-traversal guard. Replaced with `tempfile.mkstemp`,
which (1) opens with `O_CREAT | O_EXCL` semantics so any existing
dirent (including a symlink) at the target name aborts the open, and
(2) generates a randomized name so an attacker can't pre-plant
anything there. The temp file still lives inside the validated
target_dir and `os.replace` still does the atomic swap onto the final
name.
P2: `showConfirmModal` had no cancel callback. The recovery toast's
"Discard offline copy" handler wrapped it in a `new Promise(resolve =>
…)` that only resolved on confirm, so cancelling left the toast button
stuck in `"Discarding…"`. Added `onCancel` parameter; the recovery
toast now resolves `{ dismiss: false }` on cancel, re-enabling the
button. Also closes the second half of #84 — `dropPlaceholdersForPacks`
in canvas_io.js had the same Promise-leak shape and the missing
cancel callback was the root cause.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>