Commit Graph
133 Commits
Author SHA1 Message Date
Catubba e5d0d5cf58 feat(jobs): FIFO run queue and per-PBS power lease
Per-route schedules mean two routes can target the same PBS minutes apart,
so no single cycle can decide when the box goes back to sleep.

Add a FIFO run queue to JobService (enqueue/pending/current/dequeue, backed
by a drain worker) and a PowerLease that refcounts each PBS: the first holder
wakes it or finds it awake, the last one powers it off, and only when the run
succeeded, the device manages its power, and no queued route still needs it.
Sync routes hold two leases, released independently. An unmanaged PBS is
probed, never woken, never powered off.

The queue sits beside the existing single-run lock rather than replacing it:
the worker takes the same lock, so queued runs and the 0.9 entry points still
serialise against each other while the cycle, scheduler and API are ported.
2026-08-02 01:18:26 +02:00
Catubba 67e68dea1e feat(db): route dimension, per-PBS stats and an idempotent schema upgrade
create_all creates missing tables and never touches one that already exists,
so a column added to a model appeared on a fresh install and was simply absent
on every upgraded one. db/upgrade.py closes that gap without Alembic: it runs
from init_db before create_all, diffs PRAGMA table_info against the model
metadata and emits ALTER TABLE ... ADD COLUMN. Being metadata-driven rather
than a list of DDL, a later change only has to add the column to its model.

runs gains route_id and route_name, both nullable - a run recorded before 1.0
has no route, and a manual one-off need not belong to one. route_name is
denormalised so history still reads correctly after its route is deleted.

guest_backups and datastore_stats gain the device dimensions their keys were
missing: a vmid is only unique within one PVE and can be backed up to several
PBSs, and two PBSs may each have a datastore called "backup". That changes
their primary keys, which SQLite cannot ALTER, so the upgrade drops and
recreates them. Both are caches the backup cycle re-upserts whenever it has
the PBS awake, so the cost is that the dashboard shows no datastore usage and
no per-guest last-backup dates until the next cycle runs, after which it heals
itself. Run history is never dropped.

Two limits are handled rather than hit: a NOT NULL column with no default
cannot be added, so it is logged and skipped instead of raising, because a
schema that cannot be fully repaired must not stop the app from booting; and
the diff is add-only, so columns removed from a model in the past stay on disk
harmlessly.

The cache helpers take the new ids and the four call sites pass empty strings
for now, marked TODO(M05): the cycle is still driven by the 0.9 config, and
writes and reads use the same placeholder so the cache stays self-consistent.
They must be replaced together, since changing only the writer would silently
empty the cached values the dashboard reads.

Verified against a real pre-upgrade database as well as a synthetic fixture:
runs, run_steps, log_events and task_log_lines all preserved, and repeat runs
of the upgrade apply nothing.
2026-08-02 00:06:10 +02:00
Catubba a81d712dd5 feat(config): auto-migrate a 0.9 config to the route model
On the first start after the upgrade, a config.yaml written by 0.9 is copied
to config.yaml.pre-overhaul.bak and converted: pve -> pves[0], pbs -> pbss[0]
(taking over the External watch timeouts, which describe that box rather than
the job), and the backup job -> one route, plus a second verify route when
scheduled verification was on. External-schedules mode produces a route of kind
external instead of backup. Existing users notice nothing.

The converted config is validated before anything reaches disk. Any failure
logs and starts on the untouched original, so a config that cannot be converted
can never stop the app from booting. The backup copy is written once and never
overwritten, and a partially configured 0.9 install degrades rather than
producing an invalid config: no host means no device, no storage means no
backup route, no MAC means the PBS migrates as managed_power false.

Two mappings cannot be faithful and say so at WARNING. Guest mode "exclude"
widens to "all", since inverting the list needs a live guest list that is not
available at load time and widening cannot drop a guest from a backup. A cron
the time+days form cannot express - a day-of-month or month pattern, a step
value, a weekday range or names - is preserved verbatim in the new
schedule.cron field, which wins over time/days when set. The parser is a port
of the frontend cron helper, additionally rejecting non-integer and
out-of-range minute/hour values that it used to mangle into a wrong schedule.

Detection keys off the presence of the routes key rather than a non-empty list,
so deleting the last route does not bring one back on the next start. The 0.9
sections are left in place: they are still what the cycle and scheduler read
until those are ported.

Adds RouteOptions.reverify_days, which a migrated verify route would otherwise
have discarded silently.
2026-08-01 23:26:11 +02:00
Catubba c58d8b5002 feat(routes): config model for pves/pbss/routes
Add the v1.0 route model alongside the 0.9 single-PVE/single-PBS sections.
Devices are listed once under pves[]/pbss[] and referenced by id; a route is
"sources -> target + schedule" in four kinds (backup, sync, external, verify).

Validation fails clearly and names the offending id: kind coherence (a backup
route needs PVE sources, a sync route needs a different source PBS, external and
verify take none), unique ids, resolvable references, a per-PVE storage mapping
for every backup route target, and External routes rejected onto a PBS whose
power Joulenap does not manage.

The 0.9 sections stay live for now: nothing imports their classes, so every
consumer reads them by attribute and would break at runtime rather than at
import. They are removed by the milestones that replace them - pve/pbs/backup
with the backup cycle, maintenance.gc/verify with the sync/external/verify
cycles.

config.example.yaml ships the three lists empty with the full shape in comments;
it is copied verbatim on first run, so a sample entry would appear as a phantom
device on a fresh install.
2026-08-01 22:50:56 +02:00
CatubbaandGitHub d2bf47579b Merge pull request #28 from Joulenap/feature/external-schedules
External schedules mode; bump to 0.9.0
v0.9.0
2026-07-29 09:59:00 +02:00
Catubba 59a281ae4e feat: external schedules mode; bump to 0.9.0
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
2026-07-29 09:54:52 +02:00
Catubba c6125983ef feat: public demo build (npm run build:demo)
A self-contained copy of the UI that answers its own API calls from
fixtures, for hosting as static files at joulenap.com/demo: real clock,
fixtures slid onto today's calendar, an orange banner making clear the
data is fake, and a scripted backup cycle (demoTimeline.ts) replayed
live when the visitor presses Run backup now.

Only --mode demo ships the stub; the default build that CI and the
Docker image run drops it entirely. Brand asset paths now honour
BASE_URL so the demo can live under /demo/.
2026-07-29 09:50:48 +02:00
CatubbaandGitHub b4a443ba01 Merge pull request #26 from Joulenap/feature/notification-fields
Richer notification bodies, run id in history (0.8.0)
v0.8.0
2026-07-25 22:42:16 +02:00
Catubba 6bff430cf8 feat: richer notification bodies, run id in history; bump to 0.8.0
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.
2026-07-25 22:35:48 +02:00
CatubbaandGitHub 7832e11338 Merge pull request #25 from Joulenap/feature/light-theme
Light theme with header toggle (0.7.0)
v0.7.0
2026-07-23 23:37:46 +02:00
Catubba 5b3da78d20 feat: light theme with header toggle; bump to 0.7.0
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.
2026-07-23 23:37:24 +02:00
Catubba f901d0180d docs: refresh README screenshots to 0.6.0 2026-07-23 19:26:10 +02:00
CatubbaandGitHub 1c5bd28246 Merge pull request #10 from Joulenap/dependabot/npm_and_yarn/frontend/typescript-7.0.2
chore(deps-dev): bump typescript from 6.0.3 to 7.0.2 in /frontend
2026-07-23 12:08:30 +02:00
dependabot[bot]andGitHub f646ccd87f chore(deps-dev): bump typescript from 6.0.3 to 7.0.2 in /frontend
Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 10:07:05 +00:00
CatubbaandGitHub a6c2de54b9 Merge pull request #21 from Joulenap/dependabot/npm_and_yarn/frontend/vite-bc46fa1479
chore(deps-dev): bump the vite group across 1 directory with 2 updates
2026-07-23 12:06:09 +02:00
dependabot[bot]andGitHub 04d7a2a89f chore(deps-dev): bump the vite group across 1 directory with 2 updates
Bumps the vite group with 2 updates in the /frontend directory: [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `@vitejs/plugin-react` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react)

Updates `vite` from 8.1.4 to 8.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vite
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vite
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 10:03:22 +00:00
CatubbaandGitHub 44f50bc191 Merge pull request #23 from Joulenap/dependabot/npm_and_yarn/frontend/react-479fbeca4e
chore(deps): bump the react group across 1 directory with 2 updates
2026-07-23 12:01:40 +02:00
dependabot[bot]andGitHub 2a8a9ec6fb chore(deps): bump the react group across 1 directory with 2 updates
Bumps the react group with 2 updates in the /frontend directory: [react](https://github.com/react/react/tree/HEAD/packages/react) and [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `react-dom` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 10:00:14 +00:00
CatubbaandGitHub 46791e6c58 Merge pull request #18 from Joulenap/dependabot/npm_and_yarn/frontend/i18n-01d9ea4535
chore(deps): bump the i18n group across 1 directory with 2 updates
2026-07-23 11:58:10 +02:00
CatubbaandGitHub a677d55baf Merge pull request #22 from Joulenap/dependabot/github_actions/actions/setup-python-7
chore(deps): bump actions/setup-python from 6 to 7
2026-07-23 11:58:07 +02:00
CatubbaandGitHub 7a57f7ef98 Merge pull request #17 from Joulenap/dependabot/github_actions/actions/setup-node-7
chore(deps): bump actions/setup-node from 6 to 7
2026-07-23 11:58:04 +02:00
CatubbaandGitHub 302e347ff1 Merge pull request #24 from Joulenap/release-0.6.0-history-cancel-metrics
release: 0.6.0 — run history, job cancellation, Prometheus metrics
v0.6.0
2026-07-23 11:28:18 +02:00
Catubba 530cb5e9c3 release: 0.6.0 — run history, job cancellation, Prometheus metrics
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.
2026-07-23 11:20:00 +02:00
dependabot[bot]andGitHub f8d5a80e3e chore(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 08:43:51 +00:00
dependabot[bot]andGitHub 6e3208923a chore(deps): bump the i18n group across 1 directory with 2 updates
Bumps the i18n group with 2 updates in the /frontend directory: [i18next](https://github.com/i18next/i18next) and [react-i18next](https://github.com/i18next/react-i18next).


Updates `i18next` from 26.3.5 to 26.3.6
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.5...v26.3.6)

Updates `react-i18next` from 17.0.8 to 17.0.10
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.8...v17.0.10)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: i18n
- dependency-name: react-i18next
  dependency-version: 17.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: i18n
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-22 17:35:04 +00:00
CatubbaandGitHub 26201e49d7 Merge pull request #20 from Joulenap/release-0.5.0-advanced-update-check
release: 0.5.0 — Advanced settings tab, config.yaml editor, update check
v0.5.0
2026-07-22 19:32:45 +02:00
Catubba d5f7132ee1 release: 0.5.0 — Advanced settings tab, config.yaml editor, update check
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.
2026-07-22 19:20:55 +02:00
dependabot[bot]andGitHub f698209ba7 chore(deps): bump actions/setup-node from 6 to 7
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 08:44:12 +00:00
CatubbaandGitHub be5d2c2797 Merge pull request #16 from Joulenap/release-0.4.4-ux-a11y
release: 0.4.4 — UX + accessibility remediation
v0.4.4
2026-07-12 23:22:44 +02:00
Catubba 349d1b8789 release: 0.4.4 — UX + accessibility remediation
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).
2026-07-12 23:18:00 +02:00
CatubbaandGitHub b4cac6183c Merge pull request #15 from Joulenap/fe-medium-remediation-0.4.3
release: 0.4.3 — frontend medium-finding remediation (FE-H4, FE-M1..M10)
v0.4.3
2026-07-12 17:53:19 +02:00
Catubba 344665126a release: 0.4.3 — frontend medium-finding remediation (FE-H4, FE-M1..M10)
Setup wizard validation (FE-H4): a step no longer completes and Save no longer
unlocks when a check fails (unreachable PBS, missing PBS token, empty WoL MAC).

Frontend robustness / UX (re-review FE-M cluster; FE-M3 skipped):
- FE-M1  client-side request-timeout backstop (45s), localized error
- FE-M2  unsaved-changes confirmation guard + beforeunload
- FE-M4  surface manual-action start failures inline
- FE-M5  surface scheduler-toggle failure next to the toggle
- FE-M6  fix useTaskLog overlapping-poll duplicate lines (guard + idempotent append)
- FE-M7  Localization saved-note only after save + draft resync
- FE-M8  keep last guest list on transient error + error note
- FE-M9  wizard re-save preserves hand-configured PVE port/TLS
- FE-M10 correct Integrations regenerate-key copy + read/refresh via ConfigContext

Version bumped to 0.4.3; CHANGELOG updated.
2026-07-12 17:51:06 +02:00
CatubbaandGitHub ff4ba3e7bc Merge pull request #14 from Joulenap/re-review-remediation-0.4.2
release: 0.4.2 — re-review remediation (robustness, session handling, setup banner)
v0.4.2
2026-07-12 00:09:04 +02:00
Catubba 5a958c0539 release: 0.4.2 2026-07-12 00:05:01 +02:00
Catubba 88d97b9959 feat: frontend resilience — session/stale handling, apply feedback, setup banner
Re-review remediation (FE-H2, FE-H3) plus a requested not-configured banner,
all patch-level:

- FE-H3: session/401 and stale-data handling. A central setUnauthorizedHandler
  in the api client fires on any 401 (exempting /login and /account, whose 401s
  mean wrong credentials, not a dead session); AuthProvider resets auth
  client-side so the app returns to Login with a "session expired" notice.
  useStatus flags data stale after 3 failed polls, driving a "can't reach
  Joulenap" banner that clears on recovery. ApiError's parameter property was
  rewritten to a plain field so node --test can import the client module.

- FE-H2: the Scheduler "Apply changes" action gained busy/saved/error feedback
  matching the settings tabs (disabled while saving, green saved note, the
  backend's error surfaced).

- Not-configured banner: an amber dashboard banner shown when PVE/PBS aren't
  wired up (reusing the wizard's isConfigured helper), with a CTA that
  deep-links to Settings -> Setup via a new initialTab prop.

New i18n keys (en/it, parity 306/306). tsc + 23 tests + build clean.
2026-07-12 00:02:00 +02:00
Catubba 895c7ac9f8 fix: backend robustness — missed/interrupted-run alerts, MAC validation
Re-review remediation (BE-B8, BE-R1, BE-R2, BE-C2), all patch-level:

- BE-R1: detect a scheduled backup missed while the process was down
  (in-memory jobstore has no cross-restart memory). New
  Scheduler.missed_backup_since() queries the armed job's own trigger;
  core/catchup.py anchors on the last finished cycle and, on a hit, logs
  and notifies (via the new NotificationService.send_alert, on_failure
  routed). Wired into lifespan on a daemon thread. Notify-only, no auto-run.

- BE-R2: alert when a run was interrupted by a restart. sweep_orphaned_runs
  now returns the swept runs; lifespan builds a localized interrupted-run
  message (reusing the wake/power-off check so it flags "PBS left on" only
  when the box had actually woken) and dispatches it off-thread.

- BE-B8.4: failure/abort-after-wake notifications now include the
  "PBS left powered on" line. _pbs_left_on rewritten to the accurate rule
  (WAIT succeeded AND no POWEROFF succeeded). B8.2: dashboard pbs_state
  doc-comment clarifying "backing_up" covers any active run.

- BE-C2: reject a malformed pbs.mac at save time (PUT /api/config, 422,
  changed-only) reusing wol.normalize_mac — not a load-time pydantic
  validator, which would brick startup on a legacy bad value.

New backend i18n (missed + interrupted events, en/it). 331 tests pass.
2026-07-12 00:01:44 +02:00
CatubbaandGitHub 3f94413e60 Merge pull request #13 from Joulenap/backend-robustness-0.4.1
0.4.1 — backend robustness + config secret-file permissions
2026-07-10 23:04:06 +02:00
Catubba c87b1e1c49 release: 0.4.1
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.
2026-07-10 23:01:35 +02:00
Catubba 8074043a7f fix: backend robustness + config secret-file permissions
- BE-B3: open SQLite with WAL + busy_timeout + foreign_keys via a connect
  listener, so a running cycle's frequent commits don't risk "database is
  locked" against dashboard polling, and the CASCADE FKs actually enforce.
- BE-S2: write config.yaml owner-only (0600) — it holds API tokens, the
  session key and notification secrets, so it must not be world-readable on
  the host/bind mount (matches the SSH key). Best-effort; re-asserted on save.
- BE-B5: hold the single-run lock across a manual power-off (JobService
  .exclusive()) so a scheduled cycle can't start in the check-then-act gap
  and get its PBS shut down mid-backup.
- BE-B6: if the worker thread fails to start, fail the run and release the
  single-run lock instead of leaking it (which would 409 every later run).
- BE-B7: re-arm the daily history-prune job on rearm so a runtime timezone
  change moves it into the new zone instead of the boot-time zone.
2026-07-10 23:01:28 +02:00
CatubbaandGitHub 340646b477 Merge pull request #12 from Joulenap/re-review-fixes-0.4.0
0.4.0 — keep-PBS-on manual jobs + re-review fixes
2026-07-10 22:16:02 +02:00
Catubba 3c3d8d99ac release: 0.4.0
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).
2026-07-10 21:58:48 +02:00
Catubba 0cd59dcb40 fix: address re-review findings + keep-PBS-on manual jobs
Feature — manual jobs can keep the PBS awake:
- Run backup / Run GC are now available while the PBS is asleep; they wake
  it themselves and only require that no other run is in progress.
- Manual GC runs as a full wake -> GC -> power-off cycle (run_gc_cycle),
  mirroring the verify cycle, instead of failing on a sleeping PBS.
- A "Keep PBS powered on after the job" toggle on the confirm dialog leaves
  the box on instead of powering it off, defaulted to its current power
  state (already-on stays on; asleep goes back to sleep). POST
  /api/backup/run and /api/gc/run accept an optional {keep_on} body.
  Scheduled runs always power off, unchanged.

Fixes:
- BE-S9: PUT /api/account now requires the current password, so a stolen
  session alone can't rotate the admin credentials.
- BE-B1: an invalid backup cron is rejected on save (422) and, if already
  on disk, is skipped with a warning instead of crashing the scheduler on
  every restart.
- FE-C1: the setup wizard no longer clears the stored PVE token secret on
  re-save (mirrors the PBS redaction guard).
- FE-H1: exclude guest mode is shown read-only and preserved on save,
  instead of being displayed as an include set and inverted on Apply.
2026-07-10 21:58:35 +02:00
Catubba 1cf452217e release: align every version reference on 0.3.1
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.
v0.3.1
2026-07-10 00:09:47 +02:00
Catubba 5949d6cc43 fix(dev): make the stub guard un-bypassable in any build
`vite build --mode stub` loads .env.stub, which sets VITE_STUB_API=1, so the
guard admitted the stub into a production bundle — one that patches fetch and
Date and answers /auth/status with an authenticated admin. No npm script does
this, but the file must not be able to reach a build at all.

`import.meta.env.DEV` is false for `vite build` under every mode, so the stub
now cannot ship. Verified: `vite build --mode stub` no longer bundles it, and
the dev server still serves it.

Also bump the stub's fake /health version to match the release.
2026-07-09 23:59:18 +02:00
Catubba 22f7063f2e docs: add the 0.3.1 changelog links and mention the wizard fixtures
The `## [0.3.1]` heading is a reference link with no definition, so it
rendered as literal text; `[Unreleased]` still compared from v0.3.0.
2026-07-09 23:48:07 +02:00
Catubba c4629dbe1d release: 0.3.1 2026-07-09 23:47:11 +02:00
Catubba 9691db586f docs: explain how to run the frontend against the dev stub 2026-07-09 23:45:54 +02:00
Catubba a0cfa0feb4 feat(dev): add wizard route fixtures to the API stub
The dev-only stub answered every /wizard/* route with a generic
{ ok: true }, so the setup wizard could never advance past its first
card without a real backend. Add typed fixtures for pve/connect,
pbs/check, pbs/provision, interfaces, wol/detect-mac, ssh/keygen,
ssh/hostkey, ssh/trust, ssh/install and storage/derive so the wizard
can be clicked all the way through (manual or quick mode) at any
viewport with no backend running.
2026-07-09 23:41:21 +02:00
Catubba e80930b684 feat(ui): stack the settings nav into a 2-column grid below 900px 2026-07-09 23:35:56 +02:00
Catubba be7e53b415 fix(ui): let the dashboard panels shrink below their min-content width
Grid items default to `min-width: auto`, so a `1fr` track cannot shrink below
its item's min-content. The guests panel's min-content is 378px, because the
guest-name span is `white-space: nowrap` and a nowrap span's min-content is the
whole string. That floored the collapsed track above the viewport and left the
page 1px wider than a 393px phone.

Set `min-width: 0` on the children of both dashboard rows, inside the mobile
block only. The ellipsis already on that span engages, as it was always meant
to. Desktop is unaffected: there the guests panel sits in a fixed 400px track
and the activity log's min-content is 151px, well under its share.
2026-07-09 23:10:29 +02:00