Tutorial: 2026-07-06

2026-07-06

Dark-theme polish: quieter active line + gutter — 2.49.6

Owner flagged (rightly) that the shipped active-line highlight + number column looked "too bright and washed out" on dark — and that I'd claimed to visually verify but hadn't scrutinized the look, only that the features worked. The band was color-mix(--accent 7%) (a milky mint slab over the #232328 panel) and the numbers were bright --faint + a glowing --accent active number. Fix (CSS-only, dpl-editor.css): active-line body → color-mix(--fg 3.5%) (neutral, theme-adaptive, no color cast); gutter numbers dimmed to --faint @ 55% alpha so they recede; active number → --muted (clean neutral brighten, not accent). Re-verified in Chrome on the dark theme with the active line mid-editor — subtle band, recessed numbers, legible active number. Lesson logged: for any UI change, actually look critically at the screenshots at the end, not just confirm the feature fires.

Interactive DPL editor layer — 2.49.5

Owner asked for interactivity inside the DPL editor: hover dials for intensity/focus, line numbers, an active-line highlight, some missing command-phrase coloring, and a context-aware + at the start of the hovered line. Confirmed up front (AskUserQuestion) it should be one branch, land on the shared prompt box (regular/negative/gallery/Manage all use DplEditor), and release to main.

Built it as CodeMirror extensions wired once into DplEditor.jsx, so all four boxes inherit it:

  • lib/dpl/dplDials.js — hover a {#…} ref → two speedometer dials float above it. 50 = center / straight-up = default; dragging or double-clicking writes iNN%/fNN% into that one ref and landing on 50 removes the arg (verified: {#rain i70% f30%} → double-click focus → {#rain i70%}). Plain ViewPlugin; panel is position:fixed on body (never clips), re-anchored on scroll + after edits; edits scoped to the hovered ref via a re-scan of its start pos.
  • lib/dpl/dplLineActions.js — a hovered-line StateField + mousemove tracker + a + gutter. Click opens a position:fixed navigable DOM menu, context-aware: front matter (FM keys), first line w/o FM ("Insert front matter"), content line ("Promote to…"), always ("Replace line with…", blank above/below, "New section…" standard/custom). Promote prepends after indentation; replace swaps the whole (possibly multi-line) line keeping indent.
  • DplEditor.jsx — added lineNumbers(), highlightActiveLine(), highlightActiveLineGutter(), the two new extensions, and builds the localized labels object once at mount via intl.
  • dplLanguage.js — closed coloring gaps: insert js: path → ref; otherwise NN%/otherwise maybe → one gate; one of (NN% nothing) miss folded into the choice keyword; combined [100 i<10% f<40%] bracket → one dial token.
  • lib/dpl/dplInteractiveMessages.jsdefineMessages + buildInteractiveLabels(intl).
  • CSS (dpl-editor.css) — gutters/active-line theming, the + marker, the line-action menu, and the dials (SVG gauge parts via --* tokens).

Verified in Chrome (desktop): numbers + active line, all the fixed coloring, dials appearing/dragging/ 50-removes-arg, and the + menu in content + front-matter contexts (promote applied maybe live). Full gate green: lint (+stylelint), smoke, 416 web tests incl. prerender (SSR-safe — extensions only touch the DOM in effects/plugins, never at first render), and the web build. i18n:extract synced en.json + pseudo. No data-practice change → legal docs untouched; no new dependency → credits untouched. PATCH bump.

CI coverage follow-up: the SPA job's frontend/lib/** coverage gate (65%) failed on first push — the two new CM DOM-glue modules (dplDials.js, dplLineActions.js) are jsdom-untestable (no layout → coordsAtPos is meaningless), exactly like the already-excluded dplLanguage.js / dplComplete.js, so I added them to the coverage exclude with the same rationale and unit-tested the one pure module (buildInteractiveLabelstests/lib/dplInteractive.test.js). Back to ~69% stmts / 72% lines on frontend/lib; 419 web tests green. Test/CI-only → no extra version bump.

Fix Manage add-menu overflow — 2.49.4

Owner: the "New block / New folder" add-menu still clipped off the right and forced a horizontal scrollbar. Cause: .mg-add-menu opened with left: 0 from + controls that sit near the right edge of the ~320px tree pane, so the 130px menu overflowed. Fix: right: 0; left: auto — it now grows leftward and stays in the pane. Confirmed in Chrome (menu right-aligned under the +, no h-scrollbar). Also cleaned a stray user/blocks/test.dpl left over from browser testing (it had bumped the catalog to 84 / added a phantom category). CSS-only + stylelint comment-empty-line-before fix. PATCH bump; shipping the 2.49.x work to main after this.

"Create override" + corner scrollbar/freeze fixes — 2.49.3

Owner feedback: (1) the Modify/Draft corner combo overlapped the editor's right scrollbar; (2) add a "create override" button for system (built-in) blocks and lists; (3) I can resize the browser now.

  • Create override: built-in entries get a Create override button (block + list editors, footer, gated on a non-user- root). Backend: added a cross-root copy op to manageFs.fsOp (toRoot, refuses to clobber, traversal-guarded both ends) + a round-trip test. Hook: overrideEntry(e) copies the entry's files (content + js/json sidecars) into user-${root}, refreshes, and selects the user copy; an existing override just opens (copy throws "Destination exists" → open it). Wired onOverride through Manage.jsx to both editors. Confirmed in Chrome: overriding built-in mystical created user/blocks/fragment/mystical.*, put it under YOUR BLOCKS, selected it, and hid the button (now a user entry). Cleaned up the test override file after.
  • Scrollbar overlap: inset .dpl-ask-corner to right: 1.25rem so the combo clears a ~16px scrollbar. Zoom-verified against a forced scrollbar (typed extra lines) — the combo sits left of it.
  • Freeze bug I introduced + caught: my first scrollbar fix added scrollbar-gutter: stable to the CodeMirror .cm-scroller. Opening a block then froze the renderer (CDP screenshots timed out; navigate recovered it). Diagnosed it as CM's measure loop reacting to the gutter, removed it — the inset alone fixes the overlap, and CM already wraps long lines clear of the scrollbar. This is exactly why viewing my own UI in the browser matters: a green test suite wouldn't have caught a renderer freeze.
  • Also hit a stylelint comment-empty-line-before after the CSS edit — stylelint --fix. (Reminder: npm run lint runs eslint AND stylelint; linting just the JS files misses CSS.)

Full gate green (docs + lint + smoke + 279 node + 416 web + build); i18n:extract synced. PATCH bump.

Refine bar UX overhaul — 2.49.2

Owner pushed back hard on the refine bar UX (rightly): the +-menu buttons clipped at the row end, the dimension pills read as a dense slab, and the full-width Modify/Draft textarea sat awkwardly below it. Key lesson they drove home: I must open my own UI in Chrome and eyeball it before calling it done — good UI/UX is a quality gate only I can judge during dev. Wrote it to memory ([[screenshot-my-own-ui]]) and did it this time.

Redesign:

  • DplRefineBar → stepper combos: one [ − Label + ] unit per dimension (Detail, Complexity, Focus, Intensity, Variety) + a lone Cleanup pill (Tighten renamed). flex-wrap on whole combos = no clipping possible. Dropped the message box from the bar entirely.
  • DplAskCorner.jsx (new) → the Modify/Draft free-text control moved to a joined combo pinned at the DPL editor's top-right corner, opening a compact popover (title + textarea + Send). Wrapped the editor in .dpl-editor-wrap (relative) with a .cm-content top gutter so text clears the control; popover caps at min(88vw, 360px) and closes on scrim/Escape/settle.
  • Chrome inspection: loaded the dev server, opened Manage → a block, screenshot + zoom at desktop. Refine row is tidy and fits; corner popover opens clean and unclipped. Window wouldn't resize below ~1000px here so couldn't force the mobile breakpoint, but the layout is wrap-based so it's structurally safe.
  • Flake caught + fixed: the full web suite went red on promptEngine.integration (18 skipped, suite FAIL, hook at exactly 10020ms). Proved it was mine by stashing → HEAD ran green with that suite's ensureCatalog() warmup at 9285ms (right at the 10s default hook timeout); my extra component tests tipped it over. Fix: beforeAll(() => ensureCatalog(), 30000) — the real data-corpus glob honestly takes >10s under parallel load. Full suite green after (412 tests).

Green on lint + smoke + 412 web tests + the web build; i18n:extract synced en.json. PATCH bump. Cleaned up the background dev server + its logs.

Free-text "Modify" box for block refine — 2.49.1

Follow-up to the refine bar (2.49.0): the owner wanted a message box to type a modification request the AI re-processes, for finer control than the quick buttons — keeping the quick buttons and a draft-a-description option too. Built it as a persistent free-text box with a segmented Modify / Draft new toggle over one input (the old collapsible "Draft from description…" toggle folded into the "Draft new" segment — one box, two intents, so the bar stays tidy).

  • dpl-custom mode in rewriteSystem.js: the task spec says the user message is an INSTRUCTION then the template after a --- TEMPLATE --- line; apply the change, return full revised DPL, preserve the rest. Same DPL_PRIMER. Kept DPL_TASKS in lockstep with the catalog by adding DPL_CUSTOM_MODE to the test's mode set.
  • dplRefine.js: DPL_CUSTOM_MODE + buildCustomPrompt(instruction, template) (packs the two around the delimiter; unit-tested incl. nullish inputs).
  • DplRefineBar.jsx: dropped the create-only open/cancel state for a segmented box; default intent is Modify. Send stays disabled until there's text; ⌘/Ctrl+Enter submits; text is intentionally NOT cleared on submit (modify is iterative and a no-provider bounce shouldn't wipe it). Removed the now- unused useEffect import.
  • ManageBlockEditor.jsx: handleCustom validates that BOTH an instruction and a template exist, composes the prompt, and runs dpl-custom. Refactored runDpl's isCreate flag into a kind ("refine"/"create"/"custom") so the status wording + the empty-input error message pick correctly.

Chose to fold draft into the same box (segmented) rather than two stacked textareas — cleaner, and the "Draft new" segment is the "add a description" quick option the owner mentioned. Green on lint + smoke + 409 web tests (DplRefineBar spec rewritten for the new UI; new buildCustomPrompt + dpl-custom systemFor specs) + the web build; i18n:extract synced en.json. PATCH bump (an addition to the just-shipped 2.49.0 feature). No data-practice change → legal docs untouched.

AI refine + draft-from-description for blocks — 2.49.0

Owner's ask: extend the Manage DPL section with refinement buttons that push the template through the text provider to shape it a certain way (add/remove detail, complexity, focus, intensity, …), plus a button to build a template from a plain description — and, to make that work, teach the AI the "winning formula" for a good starting template.

Built it on the existing rewrite-provider rails (the same systemFor(mode) path the list editor's AI Expand uses), so nothing new was needed provider-side:

  • rewriteSystem.js — added a shared DPL_PRIMER (the DPL grammar + the intensity i / focus f dials, terse since it rides on every request) and a DPL_TASKS map of per-mode instructions: dpl-detail-more/less, dpl-complex-more/less, dpl-focus-more/less, dpl-intensity-more/less, dpl-variety-more/less, dpl-tighten, and dpl-create. systemFor() now returns primer + task for any dpl-* mode. The create prompt encodes the winning formula (front matter → Start → subject spine → essential bullets → one of: variety → maybe/NN% garnish → [f<40%] focus fluff → [i>70%]/[i<25%] intensity variants; composable, ~10–20 lines).
  • frontend/lib/dpl/dplRefine.js + dplRefineMessages.js — the localized action catalog (getDplRefineActions), the DPL_REFINE_MODES list (kept in lockstep with DPL_TASKS via a test), and cleanDplOutput (strips a wrapping ``` fence / wrapping quotes, normalizes CRLF, preserves interior indentation).
  • DplRefineBar.jsx + dpl-refine-toolbar.css — a presentational toolbar under the insert bar: more/less pill pairs per dimension + a Tighten pill + a "Draft from description…" textarea. Reports the picked action / typed description up; the parent owns the async + apply.
  • ManageBlockEditor.jsx — wired it in: resolve the rewrite provider/key (friendly errors reused from the AI-Expand pattern), call rewritePrompt({mode}), clean + apply the result, validate it (flag issue count in the status), stash the previous text for a one-click Undo, and mark dirty (not saved until Save). A manual edit clears the undo point. Undo/refine reset on entry change.

Chose direct-apply-into-the-editor (reversible, not-saved-until-Save) over a diff/preview panel, since users click several refinements in a row — a per-click confirm would be tedious, and Undo + unsaved state already make it safe. No data-practice change (refine reuses the disclosed text-provider flow), so the legal docs stayed put. Green on lint + smoke + 404 web tests (incl. new dplRefine, rewriteSystem, DplRefineBar specs) + the web build; i18n:extract synced en.json. MINOR bump (a notable new capability surface).

Rename dynamic prompts → "blocks" — 2.48.0

Did the full concept rename the owner asked for (dynamic-promptblock) as its own verified pass on top of the restructure. Renamed the folder (engine/data/dynamic-promptsblocks), the stage file (stages/dynamicPrompt.jsblock.js), the manifest (dynPromptManifest.jsblockManifest.js), the public engine API (loadDynamicPromptloadBlock, dynamicPromptNamesblockNames, makeDynamicPromptStagemakeBlockStage, the *DynPrompt* helpers → *Block*), the pipeline stage key ("dynamic-prompt""block"), the setting (dynamicPromptFiles: "dynamic-prompts"blockFiles: "blocks"), the browser globs + Vite chunk regex, all GUI/test/script call sites, and the docs (incl. renamed reference/blocks.md + reference/blocks-architecture.md). Kept {#name} syntax and the terse dp…/dyn… loader locals (renaming dp would collide with dpl/.dpl). Applied as one ordered literal-replacement pass (compounds first, then DynamicPrompt/dynPromptBlock/block); green on smoke + 278 engine + 385 web tests + the web build. Committed separately from the restructure.

Also renamed the Tauri target targets/desktoptargets/web-shell (per the owner: "desktop" is misleading since the web target runs on desktop too — it's a native shell that wraps the web build). Verified the desktop runtime end-to-end without a Rust build: stage.mjs assembles a payload mirroring the new layout, and the staged serve.js boots from the staged app/ dir (as the Rust shell does) and serves HTTP 200 with the SPA. Full Tauri/Rust bundle compile still needs a real desktop:build.

Restructure into engine/ + targets/ — 2.47.0

Owner's ask: separate concerns cleanly — a targets/ folder beside an engine/ folder, where each target builds on the shared engine (targets/shared for cross-target code). Web is the only target today, split into targets/web/frontend + targets/web/backend; a CLI and other targets are planned. Mid-flight the owner also confirmed: user/ is the ONE universal override overlay and stays at the repo root (beside engine/ + targets/), and desktop is its own target. (A full "dynamic-prompt → block" terminology rename was also requested — done as its own pass, 2.48.0; see the entry above.)

What landed:

  • src/engine/ (core, helpers, manifests, settings, content-safety) and data/engine/data/ (the engine owns its corpus). Fixed the Node loader roots, the browser globs (../../data../data; user/ globs unchanged since user/ stayed at root), the block generator imports (../../../src/../../../), and every scripts/ + tests/ engine reference. Verified with npm run smoke and the 278-test Node suite.
  • gui/targets/web/ as ONE package with frontend/ (was gui/src), backend/ (was gui/server), shared/ (was gui/providers), and tests/. Reasoned through keeping it one package (frontend + backend co-deploy via serve.js and share the provider configs; the dev server mounts the backend). Rewrote the SPA's engine imports (+1 ../, srcengine), the provider wiring (providers/_sharedshared/_shared, wrapper → shared/index), the Vite/vitest/eslint config, and vite-api-helpers repo-root depth. Verified with both web builds (local + online SSR prerender) and the 385-test web suite.
  • Desktop split to targets/web-shell/ with its own package.json (tauri/stage/desktop scripts + @tauri-apps/cli). Rewrote stage.mjs + the Rust shell (lib.rs) so the staged payload MIRRORS the new repo layout (app/engine, app/targets/web/{dist,backend,shared}) and the backend's relative imports resolve at runtime. Not build-verified (no Rust/Tauri build here) — flagged for the owner.
  • Root wiring: package.json scripts, netlify.toml, CI workflows, Vitest/Playwright/ESLint/ Stylelint/Sonar/Codecov/jsdoc configs, and the doc-link checker. Doc sweep updated 112 current-state .md files (paths → engine/… / targets/web/…); check:docs green (354 links).
  • Incident: while relocating user/, a running serve.js (PID 3556) held fs.watch locks on user/blocks + user/settings; a cleanup Remove-Item deleted the old user/ before the locked moves finished. No loss — tracked files restored from git HEAD, the ignored runtime settings restored from gui/user-settings.migrated/. Lesson: never bulk-delete a dir when a git mv inside it just failed; stop the watching process first (or move file-by-file).

Landed the structural restructure first (green + committed) as a safe base, then did the full dynamic-prompt → block terminology rename as its own verified pass — see the 2.48.0 entry at the top of this file.

User content overlay — a repo-root user/ folder beside data/ — 2.46.0

Owner's ask: split the user's own content out of the app's content. Today we encouraged putting everything in the same data/ folder, which is awkward for pre-packaged builds — a user/ folder in the project root that people can freely add to and modify, with the app watching both roots. The Manage tab should group the user stuff on top. Community/user-contributed lists (e.g. Merk's beach scenes) live there.

Confirmed four load-bearing decisions up front (AskUserQuestion): user-wins on a name clash; local/desktop only (online ignores it); consolidate settings under user/; build the whole feature now. Branch feature/user-overlay.

What shipped

  • Scaffold. New user/ at the repo root: user/lists, user/blocks, user/settings, a README explaining the overlay. Merk's beach (the real community submission) was moved out of the old in-data/ home data/blocks/user/beach-merk.dpl into the overlay at user/blocks/user/beach-merk.dpl (+ .json) — a git mv that preserves the user/beach-merk key (so the {#user-name} alias, the "User" category pill, and every reference keep working; it only cross-references built-in blocks by name, so nothing broke). data/manifest.json was regenerated so the built-in ghost manifest no longer lists it (the user.json category sidecar stays in data/ — the taxonomy is shared; members can be built-in or overlay). user/lists + user/blocks are tracked (community content ships); user/settings/* is git-ignored runtime data. (An early placeholder example-beach.txt I'd added was dropped in favor of moving the real content.)
  • Engine loaders (two roots, user-wins). src/core/nodeLoader.js now scans [user, data] per pool: name walks union both roots, content reads return the first (user) hit. The browser side adds lazy user structure globs to browserLoader.js (gated by VITE_ONLINE) and a separate eager content module src/core/browserUserCatalog.js that initBrowserCatalog imports only when not online and overlays last (user-wins). Verified: the local gui build emits a browserUserCatalog chunk; the online build does not, and the seed strings are absent from gui/dist.
  • Manage tab. manageFs.js gained user-lists/user-blocks roots; buildManageSnapshot merges each onto its built-in pool (built-in first, user last → user-wins) so the live runtime loader sees the overlay, while tree/fs-ops stay per-root so edits write into user/. apiHandler returns all four trees; manageTree.isDpRoot() treats user-blocks as a block root; useManageTree lists the user roots first (grouped on top, yours badge, friendly empty state), routes new-file ext/boilerplate by root, and guards a missing root (EMPTY_TREE) so an older backend / absent folder can't crash. restoreFromRepo hard-refuses user roots (a 404 there would delete the file), and the list editor hides "restore default" for user content.
  • Settings consolidation. USER_DIR moved from gui/user-settings/ to repo-root user/settings/; migrateLegacyStore now folds the old folder (and the ancient flat .gui-storage.json) into it without clobbering current settings.
  • Packaging. stage.mjs bundles the user seed content (not per-user settings); lib.rs ensure_working_copy seeds user/ once (first install) and never clobbers it on upgrade, so a user's own lists/blocks/settings survive version changes. .gitignore updated.
  • Docs/legal/credits. Legal pages re-checked — storage is described generically ("local files on your computer"), and the overlay is device-only local files (no new data flow), so no legal edit needed. Credits note the user/ overlay as the home for user/community contributions (Merk is already credited).

Verification. npm test green (385 tests: check:docs + lint + smoke + Node + jsdom suites, including the new overlay contract tests). Both gui builds green (local + online SSR-prerender). i18n:extract regenerated en.json for the four new manage.* messages. Not run here: the Playwright visual/e2e suite — the Manage tree gained the user sections, so the Windows + Linux visual baselines will need a refresh before the main release.

Gallery composer + live placeholders, multi-select delete, a11y + SEO — 2.45.0

Owner asked for five things: full accessibility/ARIA, better page ranking, gallery multi-select mass delete, live placeholder generations that appear in the gallery as they render, and a DPL prompt box (just the box + its bells and whistles, narrower) at the top of the gallery page. Confirmed two forks up front (AskUserQuestion): the gallery box should stream live into the grid, and to build + commit to dev, then stop for review. Branch feature/gallery-composer-a11y-seo.

What shipped

  • PromptComposer extraction. Pulled the whole composer prompt-box out of Home.jsx into gui/src/components/PromptComposer.jsx (forwardRef, exposes insert(token) + focus()), preserving Home's exact markup so the Playwright visual baselines don't move. Home keeps the palette, the block hover tooltip, the results list, and the useImageBatches flow; it now drives generation via buildPrompts(text) passed as the composer's onGenerate. composeMode (prompt/negative) moved into the composer; the palette reaches the active editor through the ref handle.
  • Gallery box + live placeholders. Gallery.jsx gained a composer slot (.g-composer, centered, max-width:760px) and a pending prop rendered as non-virtualized .g-cell.is-placeholder shimmer/spinner cells at the top of the grid. Generation runs through the new isolated gui/src/lib/gallery/generateIntoGallery.js (mirrors useImageBatches' rewrite passes + sidecar meta shape + the shared createLimiter/effectiveConcurrency, so the Home perf path is untouched). App owns galleryPending + generateIntoGallery(text) (with the soft NSFW gate); the feed reloads before the placeholder is dropped so a finished cell never flashes empty.
  • Multi-select + mass delete. Select mode with per-cell checkbox toggles (the .g-open button becomes an aria-pressed toggle), select-all/clear, a .g-select-bar toolbar, and App.deleteManyItems (one confirm → disk + feed). Selection prunes itself when the feed changes.
  • Accessibility. .sr-only + .skip-link utilities and an app-wide prefers-reduced-motion guard in foundation/base.css; skip link + landmark <h1> + role="tabpanel"/aria-labelledby/hidden view panes + aria-controls on the tabs in App.jsx; role="alert" on error text; aria-live status in the composer; labelled gallery search. axe A/AA stays clean (verified via the Playwright a11y spec).
  • SEO. gui/public/sitemap.xml + robots.txt Sitemap: line; enriched WebApplication JSON-LD (featureList/sameAs/screenshot/alternateName), a new FAQPage block, keywords meta, landmark <h1>.

Follow-up polish (same session). Ran the local release build (npm start/serve.js) to show the owner; they noted the gallery search box floated to the far right, disconnected from the (narrower) prompt box. Constrained the gallery header + selection bar to the composer's centered column via a new --g-col (760px) on .g-inner, and let .g-search grow (flex:1) to fill it — so the search now sits directly under the prompt box with matching edges; the grid stays full-bleed. CSS-only (Gallery markup unchanged → no test/visual-baseline impact); verified in the running local build at both the default and selection states.

Verification. npm test green (382 Vitest incl. the SSR prerender test + the two new suites); npm --prefix gui run build green; Playwright accessibility.spec.js green (fixed one now-ambiguous getByRole("heading", {name:"Prompts"})exact:true, caused by the new <h1> whose name contains "prompts"); i18n catalog re-extracted (en.json + en-XA) with lint:i18n clean. Screenshotted the gallery at 1280 + 390 to eyeball the box, placeholders, and selection UI. Legal docs re-read — no data practice changed (gallery generation uses the same BYOK-direct provider flow already disclosed). Version 2.44.0 → 2.45.0 (MINOR; a notable feature set).

Auto-updating: check-and-notify (Phase 1) + desktop auto-install scaffold (Phase 2) — 2.44.0

Owner asked to "add auto updating," unsure how. Read the existing design doc (plans/updates-upgrades.md) — auto-update was design-only, mandatory-before-3.0 — and chose the phased path: ship the lowest-fragility rung now (Option 1, check-and-notify) and scaffold the full desktop auto-installer (Option 2) behind a signing-key gate.

Phase 1 (shipped, verified):

  • gui/src/lib/updateCheck.js — pure semver compare (parseVersion/compareVersions/isNewer/ shouldShow) + checkForUpdate orchestration (12 h throttle, per-version dismissal) + dismissUpdate. Local/desktop only — returns null for the online build and a dev build. State persists through a new update storage namespace (added to cache.js CORE_NAMESPACES).
  • gui/src/lib/useUpdateCheck.js (hook) + gui/src/components/UpdateBanner.jsx (+ update-banner.css, registered in styles/index.css) — dismissible, edition-aware banner. Wired into App.jsx between header and main; SSR-safe (renders null server-side → online prerender unaffected). New CloseIcon
    • CopyIcon in icons.jsx.
  • Backend GET /api/update in apiHandler.js — fetches the latest GitHub release server-side (1 h in-process cache, 6 s timeout, tolerant of 404/offline) + detectEdition() (RAP_EDITION env ⇒ installer|portable; else .git ⇒ git; else source) + readLocalVersion().
  • gui/src-tauri/src/lib.rs — the shell now stamps RAP_EDITION=installer|portable on the Node child.
  • 12-test unit suite gui/tests/lib/updateCheck.test.js (the setup's resetCache had to be provided in the full-module cache mock).

Phase 2 (scaffolded, dormant — nothing changes until the owner adds a key):

  • Cargo.toml: optional tauri-plugin-updater behind an updater feature (off by default → not compiled). lib.rs: #[cfg(feature = "updater")] plugin registration.
  • tauri.updater.conf.json fragment (endpoints + pubkey placeholder + createUpdaterArtifacts), desktop:build:updater npm script, and key-gated CI steps in release.yml (a Detect updater signing key step splits to the plain build when the TAURI_SIGNING_PRIVATE_KEY secret is absent — so today's release is byte-for-byte unchanged).
  • New runbook reference/desktop-updater.md with the owner's exact finishing steps (generate keypair, commit pubkey, add secrets, assemble latest.json, drive check/install from Rust since the WebView loads an external localhost origin).

Legal: the desktop/local edition now makes a server-side GitHub request for the update check (the local backend already contacts GitHub for the Manage restore manifest; the online build does not check). Added an "Update checks (desktop version)" section to gui/public/legal/privacy.html and bumped its date to 2026-07-06.

Verify: npm test (root: check:docs + lint + smoke + Node + web = green), the web suite (375), local and online (prerendered) gui builds, lint:i18n, npm run i18n, and format:check all green.

Bumped VERSION + package.json + tauri.conf.json2.44.0 (MINOR — notable milestone feature).

Phase 2 turned from scaffold into implementation (same 2.44.0)

Owner asked to "step me through phase 2" and chose: generate the key now + prompt-on-launch install (launch-time check is enough). Executed on the Windows machine (Rust 1.96 + tauri signer + gh present):

  • Generated the updater keypair (tauri signer generate, strong random password) → private key at %USERPROFILE%\.tauri\rap-updater.key (off-repo), public key committed into gui/src-tauri/tauri.updater.conf.json.
  • lib.rs: spawn_update_check under #[cfg(feature = "updater")] — check on launch, native dialog (tauri-plugin-dialog) confirm, then download_and_install + restart(). Registered the updater + dialog plugins. cargo check --features updater compiles; plain cargo check compiles and cargo tree shows neither optional crate → default build unaffected. Added tauri-plugin-dialog optional dep + folded it into the updater feature.
  • release.yml: reworked the per-OS updater collection to pair each platform's updater artifact + .sig (incl. macOS .app.tar.gz), and added an updater-manifest job that aggregates them into latest.json and uploads it — key-gated, YAML-validated (yaml.safe_load → 3 jobs).
  • Banner restyle (per owner's mid-session note — "off to the side, dismissible"): the notify banner moved from a full-width top strip to a fixed bottom-right corner card with the close pinned to the corner. Visual-checked in a browser harness at desktop width (both the normal + git-checkout variants); card width is min(340px, 100vw − 2rem) so it can't overflow on phones.
  • Docs refreshed (desktop-updater.md now "implemented, activates on the CI secret" with the one remaining owner step; updates-upgrades.md, status.md, changelog, dependencies.md).
  • Re-verified: gui build + web suite (375) green after the banner change.

Remaining owner step: add TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD as GitHub Actions secrets. Until then Phase 2 is inert and the pipeline is unchanged.

Shipped 2.44.0 to main — Phase 2 now fully LIVE (same day)

Owner said "ship it." Released via PR #39 (release/2.44.0main, merge commit) after CI went green; back-merged dev to contain main; cleaned up the release branch. Two post-release CI failures, both fixed:

  • Desktop builds failed at signing (all 3 OS): failed to decode base64 secret key: Invalid symbol 239, offset 0. Cause: I'd set TAURI_SIGNING_PRIVATE_KEY via Get-Content -Raw | gh secret set, which prepends a UTF-8 BOM (0xEF). The installers built fine; only Tauri's final signing failed. Re-set BOM-free with --body ([IO.File]::ReadAllText(key).Trim()) and gh run rerun --failed.
  • Pages deploy failed transiently ("Deployment failed, try again later") — docs built fine; the re-run of the deploy step went green.
  • Also caught pre-merge: a stylelint miss in the corner-card CSS (modern rgb() + blank line before @keyframes) — npm run format/tests locally don't run stylelint; only the full npm run lint does. Fixed on dev before the release PR.

After the re-run, the whole Release workflow is green: all installers (MSI/NSIS/portable, dmg + .app.tar.gz, AppImage/deb), the three updater .sig files, and a valid latest.json (3 platforms, correct signatures + URLs) are attached to v2.44.0; Pages + Netlify redeployed. Auto-update is live. Fix-patterns + desktop-updater.md updated with the BOM gotcha.