Instead of a glance + 'Open backup center' button, the Backups tab now mounts
the real BackupPage (dashboard/snapshots/locations/migrate/configuration) inline,
with its sidebar restyled as a nested tab strip and its own header taking over.
- BackupPage gains an embedded mode (opts.embedded): no /backup URL coupling, so
sub-tabs switch in-page under /overview/backups. Backward compatible.
- OverviewManager lazy-loads the backup bundle + fragment on first open, news a
BackupPage({embedded:true}), and disposes it on apps-feature unmount. Colliding
ids (#sidebar/#mobile-overlay) are stripped on inject.
- Revert the Admin backup-config surface — the embedded center (incl.
Configuration) is now the single home for backup settings.
- The updater needs no equivalent: its sections were already unpacked into the
Overview/Updates/Improvements tabs + the per-row expander.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- HIGH: renderAppDetail gated the Roll back button on last_snapshot* fields no
generator emits, so it never rendered. Derive recoverability from update
history (updater_apply always snapshots first) so the affordance is reachable.
- MED: per-app Updates tab now repaints on update/rollback/check/hotfix task
completion (mirrors the backups card) instead of going stale until re-click.
- MED: in-page tab switches now sync spaClean.currentRoute, so the sidebar
Overview entry no longer no-ops after switching tabs.
- LOW: keyboard activation (Enter/Space) for the role=button expander heads,
backup tiles, and sidebar Overview entry.
- LOW: preserve ALL expanded Updates rows across a background repaint, not just
the single ?app= deep-link (split toggle into _openDetail/_closeDetail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fleet Overview area (Overview · Updates · Improvements · Backups) and the
backup center now live under App Center, so the standalone top-nav items are
redundant. Top nav is now Dashboard · App Center · Admin · Tasks.
- Remove the Backups and Updates anchors from topbar.html.
- Remove the nav{} blocks from updater/backup feature.json + manifest (so they
don't resurface when the nav kernel lands).
- Highlight App Center for /overview and /backup; drop the dead /updater branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _legacyRedirect: /updater[/tab] -> /overview[/tab] (security/recovery/history
fold into the Updates expander -> /overview/updates). /backup is intentionally
NOT redirected — it stays the operational backup center (locations/migrate/
snapshots), reached from Overview › Backups.
- Re-point the per-app hotfix chip to /overview/improvements.
- Unhide the existing backup config category in the Admin sidebar so
engine/schedule/retention config lives under Admin (same generated category
the backup center binds, so edits stay in sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New 'Updates' tab in the app detail page, beside Backups. Reuses the headless
UpdaterPage + renderAppDetail() scoped to the single app, so the per-app and
fleet views share one data/render path. UpdaterPage is added to the apps script
bundle so it's available on app pages; the tab is disabled while a task runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Open the per-app row named by ?app=<name> on load/repaint and write it back on
toggle, so an expanded Updates row is a shareable URL — mirrors the Tasks page's
?task=<id> pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a per-fleet Overview area inside the apps shell, reachable from a new
'Overview' entry pinned above the apps sidebar search. Selecting it renders a
top-tabbed view in the main pane — Overview · Updates · Improvements · Backups —
mirroring the per-app tabbed layout, with the apps sidebar persistent.
- TabController: generic root-scoped show/hide tab host (core/ui-state).
- OverviewManager: drives the 4 tabs. Reuses a headless UpdaterPage for all
update/CVE/improvement data + rendering (its renderX() are pure HTML
producers) and reads backup/dashboard.json directly for backup health.
- Overview tab: combined update + backup health cards.
- Updates tab: per-app expander table (CVEs/recovery/history inline via the new
UpdaterPage.renderAppDetail) + All/Updates/Security filter chips.
- Improvements tab: reuses the updater's signed-hotfix renderer.
- Backups tab: fleet backup-health tiles; actions deep-link per app.
- Additive only: /overview* routes on the apps feature; old /updater and
/backup pages untouched. Cleanup (redirects, nav, Admin config move) is next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update §8.7 + the banner + §1 TODOs to reflect that Phases 2–5 shipped today
(apply/revert pipeline, severity-split auto-apply, the WebUI Improvements stream
+ per-app chip, and make_hotfix.sh). Only the registry/marketplace stays
deferred (demand-gated by design).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The maintainer-side tool that turns a small hotfix SPEC into the two signed
artifacts the install verifies + applies (completes the hotfix product):
dist/<channel>/payloads/<id>.json(.minisig) the bounded declarative op list
dist/<channel>/index.json(.minisig) the catalog (entry upserted, serial++)
laid out exactly like get.libreportal.org serves it (local-serve testable).
- Reads a spec (envelope fields + an embedded ops array); inlines any
op `content_file` to content_b64 for convenience.
- Validates id charset + every op name against the applier's CLOSED vocabulary,
so a typo can't ship an artifact that fails-closed on every box.
- Builds the payload (sha256), the envelope (payload ref {kind,url,sha256,sig}),
and upserts it into index.json — bumping index_serial, refreshing valid_until
(LP_HOTFIX_VALID_DAYS, default 30), and recording the publisher in the
publishers map with role + the footprint public key.
- minisign-signs the payload + index when LP_MINISIGN_SECKEY is set (the offline
key, kept on the release machine, same as make_release.sh); unsigned otherwise
for local testing — `libreportal artifact apply` refuses to apply unsigned.
Verified end-to-end (unsigned mode): produces a valid index.json + payload.json
matching the §8.1 envelope that lpFetchIndex / artifactApply consume.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Surfaces the hotfix channel in the WebUI. Primary home is the Updates &
Improvements page (the updater component) — its own "Improvements" tab — with a
secondary chip on the App detail page (fork 3 locality = both).
Updater component (components/updater):
- New "Improvements" sidebar tab + panel; renderImprovements() reads the host-
generated artifacts_available.json (severity badge, scope chip, applied/auto/
not-applicable badges, plain-English why). Apply/Revert buttons dispatch
artifact_apply / artifact_revert through the TASK system (services.tasks.route)
— no mutating API. Apply is disabled when the index is UNSIGNED.
- Overview gains an "Improvements" stat card; task-refresh now also repaints on
artifact_* task completion; URL tab routing + dispose teardown extended.
Task plumbing (core/tasks): artifactApply/artifactRevert action methods (id is
charset-guarded before it enters the command string) + artifact_apply/
artifact_revert routeAction cases. Task list/format gain icons + friendly labels.
Apps component: an amber "⚡ N improvements" chip on an installed app's detail
header (populated async from artifacts_available.json filtered by app, applicable
& not-applied), linking to /updater/improvements. Best-effort, never throws.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- CFG_HOTFIX_AUTO (security-breakage|all|off, default security-breakage) seeded in
general_terminal; reaches existing installs via the add-only config reconciler.
- webui_artifact_scan.sh (webuiArtifactScan): fetch+verify the signed index, write
artifacts_available.json ATOMICALLY (build in temp → jq-validate → one write;
keep the prior file on any failure — never emits broken JSON). Annotates each
artifact with applied (a per-id record exists) + applicable (target installed).
- artifactApplyAuto + `libreportal artifact apply-auto`: enqueue apply tasks for
the eligible signed hotfixes — only when the index is VERIFIED-signed, only
auto==true + in the severity policy + applicable + not already applied. Each
apply is its own task (visible in the log + History), never applied inline.
- `updater check` now also refreshes the index (webuiArtifactScan) and runs
artifactApplyAuto — one front door, no second phone-home.
Unit-tested 13/13: policy filtering (security-breakage / off / all), auto:false
exclusion, already-applied skip, non-installed-app skip, unsigned-index fail-closed,
and the scan transform's signed/applied/applicable fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A 4-lens adversarial security review of the Phase 2 applier raised 19 issues
and confirmed 17 after per-finding verification. All are trust-boundary (they
require the signing key), but several break the explicit "no code-exec, always
reversible, nothing-silent" contract, so all 17 are fixed:
Trust path — fail CLOSED, never misreport:
- lpFetchIndex now surfaces the real signature state (LP_INDEX_SIGSTATE);
artifactApply REFUSES to mutate unless the index is actually verified, and
_artifactFetchPayload refuses an unsigned payload. The read path still
tolerates dev/unsigned but now says "UNSIGNED" instead of "Signed + verified".
- valid_until and index_serial are now MANDATORY + numeric in lpFetchIndex
(missing = refuse) — closes the anti-withholding / anti-rollback fail-opens.
Injection / code-exec (defense in depth even for a signed payload):
- runFileWrite rootless branch no longer builds a `bash -c` shell string with the
destination interpolated — it uses the argv form (like runFileOp), so a path
with a quote can't inject a command as the install user. (shared-helper fix)
- op paths must match a safe-filename charset (no quotes/$/backtick/;/newline);
set-config-key values and set-compose-image refs are charset-guarded too.
- content_b64 is validated as real base64 at precheck.
Reversibility / honest failure:
- dockerComposeUp now returns the real compose exit status (it always returned 0,
so the updater's rollback gate AND the apply's start-failure detection were
fail-open). (shared-helper fix)
- set-config-key undo captures the WHOLE config file (lossless) instead of a
lossy re-parsed scalar; edit-only (rejects an absent key).
- _artifactReplayUndoFile returns non-zero if any inverse op fails; auto-rollback
and revert now record "rollback-incomplete"/"revert-incomplete" + isError
instead of falsely claiming success, and revert keeps the record for retry.
- applied-record write failure is checked — apply rolls back rather than leave an
un-revertable change. System-scope regen failure is no longer swallowed.
- Writes are path-aware (configs/ -> runInstallWrite, container tree ->
runFileWrite) so system-scope hotfixes write/restore correctly.
- Checked lazy-sourcing surfaces a clear error instead of a bare exit 127.
Unit-tested 35/35 (adds: command-sub value rejection, bad image-ref, invalid
base64, quote/metachar path-injection rejection, replay-failure reporting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The mutating side of the unified distribution primitive (spec §8.3). Hotfixes
can now be applied and reverted, first-party, through the task system.
New scripts/cli/commands/artifact/cli_artifact_apply.sh:
- artifactApply <id>: resolve+gate (applies_when / min_lp / max_lp /
max_footprint / publishers-map role) → fetch+verify payload (sha256 pinned by
the signed index + minisig) → dry-precheck ALL ops (all-or-nothing) → best-
effort snapshot → apply each op recording a precise inverse → bring app up →
auto-rollback (replay undo LIFO, snapshot fallback) → applied-record + History.
- artifactRevert <id>: replay the applied-record's undo log (LIFO).
- Bounded, CLOSED op vocabulary (no run-script/exec, ever): set-config-key,
set-compose-image, patch-file-if-checksum-matches, set-data-file. An
unsupported op rejects the whole artifact at precheck (fail-closed).
- Write-target firewall: scope:app → containers/<app>/ only; scope:system →
configs/ only; the install tree (our code) is off-limits to hotfixes (fork 1).
Drift guards (expect_current / checksum) skip cleanly rather than clobber.
- Two-tier trust: index minisig-verified vs the footprint key (lpFetchIndex)
covers the envelope; payload sha256-pinned + minisig-verified; publishers-map
role gate (a non-official publisher can't claim official). Community per-
artifact-key sigs are gated off until that tier is enabled.
cli_artifact_commands.sh: apply/revert via the task system (artifact_apply /
artifact_revert types — no allowlist needed), + read-only `applied` list.
cli_updater_commands.sh:
- FIX verified safety bug: updaterApplyApp/RollbackApp called `libreportal backup
app "$app"` and `... restore latest`, which parse the app name as the ACTION,
hit the dispatcher's `*)` default (exits 0) — so updates ran with NO snapshot
and rollback was a silent no-op. Call backupAppStart / restoreAppStart directly.
- FIX updaterRecordHistory jq-silent-skip: was `command -v jq || return 0`
(silently dropped the audit entry). Now fail-closed with a brace-agnostic
bash-native prepend fallback; extended with artifact_id/serial/undo_id.
fetch.sh: add _lpJsonEsc (shared JSON-escape for the jq-free fallbacks).
Regenerated source arrays + lazy-load manifest for the new file/functions.
Unit-tested 31/31: every op apply+precheck+undo round-trip, the path-allowlist
firewall (incl. .. traversal + install-tree + cross-app rejection), all-or-
nothing abort, unsupported-op rejection, and the History bash-native fallback
(records + preserves prior entries without jq). A full signed-apply e2e needs
minisign + the signing key (Phase 5 make_hotfix.sh).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The four-lens design panel finished (marketplace-first ranked top) and
confirmed the format; graft in the strongest refinements it surfaced so
the spec is genuinely "done":
- Publishers MAP trust anchor: `publisher` is now a key into an index-root
`publishers` map ({display, role, key}) the team-signed index vouches
for, not an inline {name,trust}. An artifact's claimed trust is honored
only if the publisher's role permits AND its sig verifies against that
key — so a community key can never self-certify as official. This is the
load-bearing trust mechanism for the marketplace seam.
- Two-tier reversibility: a per-op `undo` array (precise revert) plus the
snapshot (dirty-op fallback).
- All-or-nothing dry-precheck-all before any snapshot; unknown op rejects
the whole artifact at validation.
- Canonical-bytes signing rule (sign the exact artifact bytes, never
re-serialize on the box) + warrant-canary countersigning index_serial.
- Op vocabulary grown to the full set (set-data-file as the bridge to
bundles; set/unset-compose-env; ensure-compose-up/restart-service).
- Envelope gains version/supersedes/reversible + richer applies_when
(image_match/requires/conflicts).
- CFG_HOTFIX_AUTO + staged rollout / randomized delay / recall-via-supersedes.
- Flag the VERIFIED existing bug: updaterRecordHistory silently skips the
audit entry when jq is absent (cli_updater_commands.sh:154-168) — Phase 2
must make it fail-closed; "nothing silent" depends on it.
- Phases re-sequenced (P2 heart, P3 auto-apply, P4 WebUI, P5 make_hotfix.sh,
deferred registry).
Spec-only change — no code; the Phase 1 read primitive is unaffected (it's
a generic verified fetch; publisher/envelope internals are Phase 2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
curl's raw "(6) Could not resolve host" / 404 noise leaked through on
the index.json download while the .minisig fetch was already silenced —
inconsistent and confusing. The caller's clean isError covers the
failure, so route the index download's stderr to /dev/null too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Build the read side of the unified distribution primitive from
docs/roadmap/updates-and-distribution.md: one team-signed catalog
(index.json) on the same channel as latest.json, listing type-tagged
artifact envelopes. A hotfix is the first artifact type; apps/themes/
components are future envelope rows through the SAME pipe — the
marketplace seam is just the `type` + `payload.kind` fields.
Phase 1 is fetch + verify + parse only (NO mutation; the snapshot →
ops → rollback → History apply verb is Phase 2):
- Factor `lpVerifyMinisig` out of `lpFetchRelease` (scripts/source/
fetch.sh) — one trust anchor (the root-owned footprint key) now
shared by releases and the index; refactor `lpFetchRelease` to use
it (behaviour-preserving, still fail-closed).
- scripts/source/artifacts.sh: `lpFetchIndex` — download →
verify-before-parse → `valid_until` freshness (anti-withholding) →
`index_serial` monotonic high-water (anti-rollback, TUF-lite) → emit
verified JSON. Trust core is jq-free; parsing accessors prefer jq
with a grep fallback.
- `libreportal artifact index` (scripts/cli/commands/artifact/) —
read-only front door that fetches, verifies and lists. Runs directly
like `updater check` (no task; no mutation).
- Regenerate the source arrays + lazy-load function manifest for the
new files.
Doc: promote the format from vision to spec (§8) — 3 layers
(INDEX/ENVELOPE/PIPELINE), the bounded declarative op vocabulary (no
run-script, ever), the apply pipeline mapped onto existing functions,
the marketplace seam, and resolutions for all five open forks.
Self-tested 12/12: trust core fails closed (real key + no minisign →
refuse), happy path, stale-refused, rollback-refused, signature-refused,
jq + grep parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The teardown audit found the backup-stacking leak class across 4 more feature
modules (12 confirmed leaks); unmount() left document/window listeners, intervals,
and SSE subscriptions firing on stale controllers after navigation:
- admin: overview/ssh/peers/system each leaked a document click listener ->
AbortController + dispose() per page; admin unmount() aborts each.
- dashboard: the 1 Hz update-countdown interval + the LiveSystem view sub ->
stopUpdateCountdown()/detachDashboardLive(), registered via ctx.sub().
- tasks: constructor-started global live-log poller (discarded handle) -> stored
+ idempotent + cleared on unmount + re-armed on mount; per-task monitorTask
window listeners + interval -> tracked in a map, released on unmount.
- apps: app-tabbed reconcile setTimeout loop + watchdog window/document listeners
+ popstate -> per-instance AbortController + dispose() that clears the timer,
resets the guards, and unloads the active tab's Services intervals + log SSE.
All mirror the kernel's MountContext teardown discipline. 12 files, all pass
node --check. Backup (fixed earlier) re-confirmed clean by the audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The original report: clicking a backup sidebar tab loaded content on top of
the old content. Root cause (flagged in the unmount comment as deferred):
BackupPage.bindEvents() attaches document-level click/input/change listeners
guarded only by the instance-level this.eventBound, and unmount() nulled
window.backupPage WITHOUT removing them. Each revisit added another full set of
listeners bound to a stale BackupPage, all firing on every click and mutating
the live DOM (double tab-switches, double modal opens, stale-instance renders).
Fix (mirrors the kernel's MountContext pattern): give BackupPage an
AbortController, bind the three document listeners to its signal, add dispose()
that aborts them (+ drops the task-refresh reg + clears the timer), and call it
from the feature module's unmount(). Revisits now start clean — one live
instance, one set of listeners.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The honest-checkSuccess + masking fixes immediately surfaced a real masked
failure in error_report.log: updateDockerSudoPassword (run every system scan
from start_scan.sh) does 'sudo passwd $sudo_user_name', but Model A's scoped
sudoers grants only LP_HELPERS/LP_SYSTEM + run-as-install-user — not passwd.
So at runtime (manager, non-root) it failed exit 1 every scan, masked until now.
The password is set at install (root, chpasswd) and admin login is key-based,
so the runtime re-sync is legacy + impossible under de-sudo: guard it to skip
unless EUID 0. (Validates the surfacing mechanism working as intended.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
'local result=$(cmd)' resets $? to 0 (the local builtin's own exit), so the
following checkSuccess always saw success regardless of cmd's real exit — the
mechanism that masked the de-sudo write failures. Split declaration from
assignment ('local result; result=$(cmd)') across all 235 active-code sites
(84 files) so the command's exit reaches checkSuccess. No behaviour change
beyond $? now being accurate (no set -e in runtime code; multi-line
assignments transform safely).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
checkSuccess silently reported '✓ Success' for failed commands, which is how
the de-sudo write gaps (throttle stamp, passwords, updater) hid. Rework it:
- Capture the real exit code up front; success path unchanged.
- On failure, ALWAYS append to a greppable $logs_dir/error_report.log tagged
with the caller's script:line + exit code — a failure can't hide behind a
green check anymore.
- New CFG_REQUIREMENT_CONTINUE_ON_ERROR (default true): log + continue so one
failure doesn't abort the run and we surface EVERY issue in a single pass.
Flip it off later for strict abort/prompt (the prior behaviour, preserved).
Documents the 'local VAR=$(cmd); checkSuccess' footgun (local resets $?), which
the next commit fixes across the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
updater_check/apply/apply_all/rollback tasks fell through every per-type
branch of the Tasks panel, so they showed the generic custom gear icon, a
raw/truncated command title, and (for the app:'updater' sentinel) a broken
hidden app icon. Wired them in like every other task type:
- tasks-format.js formatCommandForUser PATTERNS: added the 'libreportal updater'
command rows (Apps - Check for Updates / Update All / <App> - Update /
<App> - Roll Back) — only the *self*-update 'libreportal update' was mapped.
- tasks-format.js formatActionTitle: added the updater_* short labels.
- tasks-list-render.js getTaskTypeIcon: updater_check 🔍 / apply ⬆️ /
apply_all ⬆️ / rollback ↩️ (reusing existing verify/update/restore classes).
- tasks-list-render.js renderTaskIcons: treat app:'updater' as a sentinel like
app:'system' so updater_check/apply_all fall back to the LibrePortal logo
instead of a 404'd /core/icons/apps/updater.svg (apply/rollback keep their
real app icon).
node --check clean.
Signed-off-by: librelad <librelad@digitalangels.vip>
Two more cases of the manager writing directly into the container-owned
/libreportal-containers tree (same class as the regen-poll stamp), both masked
by a '✓ Success' that printed anyway:
- Password replacers (config/password/*): used 'runInstallOp sed -i' (manager)
on app configs copied into the container tree, so sed -i EACCES'd its temp
file and the substitution silently failed — the adguard.config 'couldn't open
temporary file', leaving the literal RANDOMIZEDPASSWORD placeholder. Added
runCfgOp (picks runFileOp vs runInstallOp by the target file's location) and
routed every $file grep/sed/awk through it: password, username, hex, vapid,
appkey, and bcrypt.
- Updater generator (webui_updater_scan): 'runFileOp cp <manager-tmp>' can't
read the manager's 0600 mktemp as the container user, so it fell through to a
manager 'cp' that EACCES'd on the container-owned out_dir. Switched the three
writes to 'runFileWrite < tmp' (manager shell reads the tmp; container user
tees the write).
Both deploy via the normal quick path (relocatable scripts) — no footprint bump,
no reinstall.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A self-referential array — files_source.sh enumerates the arrays/ files — only
picks up a newly-created arrays/ file on the next regen pass. The task-folder
move created files_task.sh; this pass adds it to source_scripts so the committed
arrays match a fresh regen (and make_release's stale-array guard stays happy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
maybeRegenPoll truncates $REGEN_POLL_STAMP (.regen_poll_at) to throttle the
self-heal 'regen webui' poll, but the stamp lives in the docker-install-owned
TASK_DIR — the manager-run processor can't write there, so the truncate
EACCES'd every poll (swallowed by || true). The stamp never updated, so the
throttle read last=0 forever and 'regen webui' ran on every idle tick (and
spammed the journal ~16x/min).
Fix: pre-create the stamp world-writable in setupTaskDir, exactly like the
lock file and FIFO already are (runFileOp install -m 666). Truncate then
lands, the mtime advances, and the poll throttles to REGEN_POLL_INTERVAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>