It is a page of instructions for something most readers will never do, sitting
in the middle of the upgrade section. Collapsed, the upgrade notes stay short
and the steps are one click away for the person who needs them.
The summary line carries the conclusion -- config rolls back by itself, the
database needs a step -- so a reader learns the important half without opening
it. First body sentence dropped as a duplicate of that summary.
The upgrade section promised "the .bak is your rollback" and never said what to
do with it. The config half is straightforward -- the backup is the file 0.9
wrote. The database is not: 1.0 widened guest_backups and datastore_stats with
NOT NULL columns 0.9 never writes, so 0.9 on a 1.0 database hits an
IntegrityError on both caches. The cycle swallows those (a WARN in the run log),
but resolve_datastore does not, so the dashboard 500s whenever a backup server
is awake -- exactly when someone is watching it.
Verified against 0.9's own code from the tag, on a 1.0 database: both cache
writes fail before the drop, both succeed after it, and the run history survives
either way. Dropping the two caches is enough because both versions rebuild them
from the backup server.
Also states what the failure looks like if the config step is skipped: 0.9's
model forbids unknown keys, so it refuses to start rather than coming up empty
and later saving over the 1.0 file.
The readme, the changelog and the architecture notes all told a first-time
reader that two datastores on one backup server carry independent power leases
and can therefore sleep and wake that machine once more than needed. Keying the
lease on the host removed it, so what shipped in three files was no longer true.
The architecture notes now describe the key itself, and what a sync route
between two datastores of one machine actually does: one lease, one wake, one
power-off.
Namespaces appeared nowhere in any shipped file. Joulenap lists a datastore's
root namespace, and a PBS namespace is configured on the Proxmox storage entry
where Joulenap never sees it, so a namespaced setup backs up, prunes and
collects garbage correctly while every one of its guests reads "never backed
up". Silence made that a wrong number with no explanation; for a backup tool
it is the worst direction for a false negative.
The power lease is also keyed per device rather than per machine, so two
datastores on one backup server can sleep and wake it once more than needed
between consecutive routes. Runs are serialised, so this is waste rather than
incorrectness.
Both are in the readme, the changelog, the example config and the architecture
notes -- the four places someone configuring this actually reads.
Four review passes over the branch — backend, frontend, docs/packaging,
security — and the findings each one confirmed.
Config and secrets:
- omitting a device secret in `PUT /api/config` silently cleared it.
`deep_merge` merges mappings key-by-key but replaces lists wholesale, and
1.0 moved devices from mappings into lists, so deleting an
`api_token_secret:` line in the YAML editor returned 200 and wiped the
token. A stored secret is now carried over when the field is absent
entirely; an explicit `""` still clears it.
- the pre-migration `.bak` was created by `copyfile` at 0644 and chmod'ed
after, leaving a window in which every credential in it was world-readable.
Opened 0600 up front, like every other secret-writing path.
Frontend:
- a failed SSH keygen put both wizards into an unbounded POST loop: the
effect depended on the whole setup object, which is a fresh literal every
render, and a failure clears the request cache so the retry fired from the
next render instead of from a click.
- an expanded run's timeline froze at the finish, leaving `poweroff` stuck on
"Running" forever, because polling stopped the moment the run did — and the
power-off steps are recorded after the run reaches success.
- the route editor merged the PVE and PBS id namespaces, which are unique
only per list. With a PVE and a PBS sharing a name, clicking the PVE chip
lit the PBS one, flipped the badge to Sync and saved a route that synced a
box to itself. The draft now carries kind-prefixed keys, as the topology
already did.
- Add-PVE could not recover when the PBS landed but the PVE create then
failed: each retry minted another orphan token on the PBS and re-posted a
device that already existed. It now resumes instead of repeating.
- the route-enable and guest-include switches had no accessible name.
- upcoming occurrences drifted by an hour across a DST boundary — a fixed
86_400_000 ms step is a UTC day, not a calendar day.
- a route whose last run aged out of the shared 50-run window claimed it had
never run.
- the wizard's manual SSH-key step showed the unrestricted public key above
the restricted `authorized_keys` line, inviting a copy of the wrong one.
Docs: the migration-failure fallback was documented backwards in three files
(the file on disk is preserved, but nothing in 1.0 reads the 0.9 sections, so
the app boots empty with the banner set); TLS pinning and SSH host-key
verification were both documented as unconditional when the runtime degrades
without a fingerprint and trusts on first use; the sync-route credential
propagation was documented nowhere; plus a dead Localization page, a wrong
endpoint for run steps, an omitted session-less endpoint, a self-contradiction
about which wizard calls write to disk, a password-change claim that
contradicted SECURITY.md, and a stale `[Unreleased]` link.
Rewrite demoTimeline.ts around the three-route scenario and rebuild the
scripted replay inside devStub.ts on top of the 1.0 endpoints, then bring
the shipped documentation in line with what actually ships.
Demo:
- demoTimeline.ts keys its online windows by PBS id, so one field covers a
single-box backup and a sync route's two; steps carry the per-device
names the backend really emits, plus their detail.
- The demo auto-plays: it opens mid-backup on Nightly and the queued Lab
route starts by itself when that lands, with the target left awake
between them and the skipped power-off recording why. The queue and the
power lease are visible without a click.
- The clock ticks and the fixture calendar shifts by whole weeks, so
weekdays and times survive and the schedules stay self-consistent. Dev
stub mode keeps its frozen clock; every replay mutation sits behind the
demo flag.
- Restore the "fake data" banner and make logout reload rather than strand
the visitor on a login form.
- build:demo now type-checks first, which it never did.
Fix the expanded run history row refetching its detail only once, so a run
in flight showed a frozen step timeline while its task log kept streaming.
Docs:
- ARCHITECTURE: the route model, the queue and lease, a cycle per kind, the
migration, and REST tables rebuilt from the shipped routers.
- CONFIG-WIZARD: the two device flows, and the /remote grant a sync route
needs on a peer configured before 1.0.
- INTEGRATIONS: the new dashboard payload, snippets matching the ones the
app generates, the labelled metric names, and a 0.9 mapping table.
- README, INSTALL: routes, the five settings tabs, upgrading from 0.9, and
the Node version CI and the image actually build with.
- SECURITY: transport pinning, auth hardening, the two API-key endpoints
outside the session, and what Joulenap deliberately does not do.
- CONTRIBUTING: npm test is a separate CI step, and the demo section now
describes the demo that exists.
- CHANGELOG: the 1.0.0 entry, including the breaking dashboard and metrics
shapes and the exclude guest mode widening to all.
Let PVE/PBS run their own scheduled jobs (backup, prune, GC, sync) and
use Joulenap purely as the power manager: at the scheduled time it wakes
the PBS, watches its tasks, and powers it off once they have been quiet
for a configurable window (issue #27).
- backup.external config block (enabled, first_task_wait, idle_wait);
both knobs are timeouts, not fixed delays — watching starts as soon
as the first task appears, and the quiet countdown restarts whenever
a new task starts, so staggered/chained jobs are never cut short
- new monitor run kind + step: wake -> wait -> watch -> power off,
recorded in run history with the number of tasks observed; a wake
where no job ever appears still powers off and warns in the
notification that the external schedule did not fire
- monitor runs count as the backup slot for last-backup status and the
missed-backup startup check
- Schedule card mode switch (Scheduled by Joulenap / External
schedules): external mode hides GC/retention/guest selection and
shows the two timers with explanatory tooltips; manual panel runs
the watch cycle instead of a backup and hides the GC button
- notification titles, body lines and duration phase for the watch
cycle, in both languages
Notifications now carry what triggered the run, a per-phase duration
breakdown, guests as ok/total with the names of the ones that failed,
the next scheduled run and the run number. Per-guest outcomes are read
off the vzdump task log as it streams, into a summary the caller owns,
so a failed task still reports which guest broke.
Also fixes two delivery bugs: bodies were collapsed onto a single line
on HTML channels (Apprise was never told the text was plain), and the
missed-backup alert mixed UTC with the configured timezone.
The run number is now the first column of the run history, so the id a
notification quotes can be looked up in the interface.
The dark/light palettes now live as CSS variables in index.css, switched by a
data-theme attribute on <html>; theme.ts tokens reference the variables so the
whole UI (CodeMirror included) restyles without re-rendering. Scattered hex
literals are folded into shared tokens, and the four hex+alpha concatenations
are replaced with color-mix so they work on variable references.
The header gains a moon/sun toggle between Settings and Logout. The choice is
persisted to app.theme through the config API (the value existed since 0.1.0
but was never read) and mirrored to localStorage, which an inline script in
index.html applies before first paint so there is no flash of the wrong theme.
Switching cross-fades over 300ms, skipped under prefers-reduced-motion.
Also: header action buttons stretch to equal height (Logout was 3px short),
the dev stub persists PUT /config edits instead of silently reverting them,
and a parity test asserts both palettes define the identical token set.
Run history in the UI: the activity card gains a second tab listing every run
with job type, trigger, result, duration and guest count. Rows expand in place
to that run's steps and log lines. Backed by /api/runs, which had existed since
0.1 with no consumer; RunSummary gains guests_ok.
Job cancellation: Run backup / Run GC turn into Stop while a job is in flight,
behind a confirmation that can also power the PBS off afterwards. Cooperative
cancellation checked in the existing poll loops (task wait, PBS wake wait,
between steps), and the underlying PVE/PBS task is stopped, not abandoned, so a
cancelled backup does not keep running on the server. A running verify is
stoppable too. Previously a stuck job blocked every later run and manual
power-off until restart.
Prometheus /metrics for Grafana, protected by the existing dashboard API key.
Sixteen gauges including per-guest last-backup times, so a guest dropping out of
the backup set can be alerted on. Written directly in the text exposition format
rather than adding a dependency; a scrape never wakes the PBS.
Notifications now name the job that ran: a failed verify or GC no longer reports
"backup failed".
Removed the dead backup.guests.auto_include_new key. It was never read, while
its name and default implied new guests were picked up automatically. Existing
configs still load (the key is stripped) and the docs now state the real rule.
Documentation accuracy pass over README, ARCHITECTURE, INSTALL, INTEGRATIONS,
SECURITY and config.example: corrected the PVE and PBS token privilege lists,
the garbage-collection and guest-selection descriptions, the supported-versions
table and the API reference, and added a Settings walkthrough.
Toggle switches are announced as switches by screen readers and can no longer
submit a surrounding form.
Closes re-review findings 11.8, 11.10 and 11.14.
- Advanced settings tab exposing the config-only knobs: backup mode,
bwlimit, keep_last/keep_yearly, history retention, port, session
lifetime and the HTTPS-only cookie flag.
- In-browser config.yaml editor (CodeMirror 6, lazy-loaded chunk) over
new GET/PUT /api/config/yaml. The PUT path reuses the extracted
_apply_config helper, so the editor gets the same redaction,
deep-merge and validation as PUT /api/config; a Copy button yields a
secret-free config for bug reports.
- Opt-in update check (app.update_check, default off): GET /api/update
asks GitHub once a day behind a 24h cache and never touches
/api/health, which the Docker healthcheck depends on.
Frontend UX and accessibility fixes from the v0.3.1 re-review, plus
self-hosted fonts.
- Accessible confirmation dialog: role=alertdialog, aria-modal, focus
trap, Escape to close, focus restored to the trigger on close
- Self-hosted IBM Plex fonts (latin subset) — no Google Fonts request;
the UI renders offline / air-gapped
- Login and first-account screens are proper forms with password-manager
autocomplete hints and a progress state on submit
- Header status label shows "GC running" / "Verify running" during those
jobs instead of always "Backup running" (new /api/status running_kind)
- Block Selective backup mode with no guests selected, with an explanation
- Setup wizard: errors on lower steps scroll into view + role=alert;
Detect MAC reports when auto-detection finds nothing
- Empty guest-list state in the guests panel
- Harden shared Dropdown buttons with type="button"
Also fixes the CHANGELOG compare links (the 0.4.3 release omitted them).
Patch release: backend robustness fixes (SQLite WAL/busy_timeout/foreign_keys,
manual power-off race, worker-thread lock leak, prune-job timezone) and a
security hardening (config.yaml written owner-only). No feature or API changes.
Bump every version reference to 0.4.0 and add the 0.4.0 CHANGELOG entry
(keep-PBS-on manual jobs + manual GC cycle; account current-password,
cron-validation, wizard-secret, and exclude-mode fixes).
Minor bump: new user-facing feature and an additive, backward-compatible
API field (keep_on defaults to false = prior behavior).
frontend/package.json had drifted at 0.2.0 since the 0.2.0 release, and the
README status heading was never updated for 0.3.0. The lockfile pins the root
version too, so it is synced here to keep `npm ci` happy.
The Docker references all use `:latest` and need no change.