Files
Codex 7960809b30
CI / Frontend build (push) Successful in 11s
CI / Backend tests (push) Successful in 16s
CI / Script syntax (push) Successful in 3s
Self-heal stale local_path pointers on read
Backups completed before the keep-local-backups feature never had
local_path nulled out after the local archive was deleted, so the new
"Local copy: Kept" indicator was trusting a stale DB column instead of
reality. GET /api/backups and GET /api/backups/{id} now verify the
file still exists and clear local_path if it doesn't, which also
covers any future case of a local file disappearing outside the app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 13:06:53 +02:00

7.9 KiB

Agent notes for PVE Cloud Backup

This project is a directly installed Proxmox backup web app. It runs from:

/opt/pve-cloud-backup/
  backend/    FastAPI app, worker, SQLite migrations, tests
  frontend/   Vue 3 + TypeScript + Tailwind source
  static/     generated frontend build; ignored by git
  data/       runtime SQLite database; ignored by git
  logs/       runtime logs; ignored by git
  scripts/    install/update/uninstall/manual integration scripts
  systemd/    source copies of service units

The live services are:

  • pve-cloud-backup-web.service
  • pve-cloud-backup-worker.service

The web app listens on 0.0.0.0:8080.

Core rules

  • Keep runtime configuration in SQLite only. Do not add .env, YAML, JSON, TOML, or other runtime config files.
  • Do not commit /opt/pve-cloud-backup/data/, /logs/, /static/, .venv, node_modules, caches, or generated artifacts.
  • Do not use Docker or add distributed-worker infrastructure.
  • Do not call vzdump directly. Use pvesh for backup operations.
  • Do not require a crypt remote. Plain OneDrive remotes and crypt remotes are both allowed.
  • Do not use rclone sync.
  • Use exact-object rclone operations:
    • upload with copyto;
    • verify with lsjson;
    • delete files with deletefile;
    • remove empty per-backup folders with rmdir.
  • Never delete a local archive unless upload completed and the remote object was verified.
  • Mock pvesh and rclone in automated tests. Manual integration testing is limited to scripts/integration-test.sh and must only run when the user explicitly asks for a real backup test.

Runtime data

SQLite database:

/opt/pve-cloud-backup/data/app.db

Important runtime settings live in SQLite, not files:

  • Proxmox node
  • Proxmox backup storage
  • local dump directory
  • rclone executable path
  • rclone remote name
  • remote path inside that rclone remote
  • timezone
  • retention defaults
  • keep local backups after upload (global toggle)
  • Discord webhook URL
  • CORS origins

Treat all node names, storage names, guest names, VMIDs, rclone remotes, and filesystem paths as operator-specific runtime data. Do not hardcode deployment-specific values in application code or publish-facing docs.

Backend map

  • backend/app/main.py
    • FastAPI app, API routes, setup/settings validation, static frontend serving.
  • backend/app/backup_service.py
    • Backup state machine, upload/delete/retention/recovery logic.
  • backend/app/commands.py
    • Shell wrappers for pvesh and rclone.
  • backend/app/jobs.py
    • Cron validation, timezone-aware next-run calculation, job CRUD helpers.
  • backend/app/migrations.py
    • SQLite schema and default settings.
  • backend/app/settings_store.py
    • SQLite settings serialization/parsing.
  • backend/app/worker.py
    • Scheduler/worker entrypoint.
  • backend/tests/
    • Unit tests. Keep external command usage mocked.

Frontend map

  • frontend/src/App.vue
    • Single-file UI for setup, dashboard, guests, jobs, history, backup details, and settings.
  • frontend/src/main.ts
    • Vue mount entrypoint.
  • frontend/src/style.css
    • Tailwind/global styling.

The built frontend goes to /opt/pve-cloud-backup/static/ via npm run build; do not commit that directory.

Backup workflow

Expected states:

  • queued
  • pve_running
  • local_ready
  • uploading
  • remote_ready
  • local_deleting
  • completed
  • failed
  • deleting
  • local_downloading
  • deleted

Important behavior:

  • The worker atomically claims queued backups before starting Proxmox work.
  • pve_running must have a real Proxmox UPID before polling task status.
  • Local archive discovery must ignore .log and .notes; only real archive suffixes count.
  • Remote path shape is:
<rclone_remote>:<rclone_remote_path>/<backup_id>/<archive_name>
  • local_deleting only unlinks the local archive when the keep_local_backups setting is off (the default). When it's on, the archive stays in local_backup_dir after completed and local_path stays populated.
  • Deleting a backup (manual delete, job-cascade delete, retention, or crash-recovery resume of a deleting backup) always removes both the remote object and any locally-retained archive, regardless of keep_local_backups — that setting only affects the immediate post-upload step, not deletion.
  • Deleting a backup removes the exact remote file and then attempts to remove the empty <backup_id> folder.
  • Deleting a backup already in deleted state purges the SQLite metadata row.
  • Deleting a job deletes all known non-active backups for that job first, then removes the job.
  • Retention runs after backup completion and hourly from the worker.
  • A completed backup whose local archive is missing (never kept, or deleted via "Delete local copy") can be re-fetched from the remote into local_backup_dir via POST /api/backups/{id}/download; this is a transient local_downloading state that resumes on worker restart. Since local_backup_dir is the actual Proxmox storage dump directory, a downloaded archive shows up in the Proxmox UI for the operator to restore from directly — this app never runs the restore itself.
  • A locally-retained (or downloaded) copy can be removed independently of the remote object via DELETE /api/backups/{id}/local, without affecting state or remote_path.
  • local_path is not trusted blindly: GET /api/backups and GET /api/backups/{id} verify the file still exists on disk and clear local_path in SQLite if it doesn't, so a stale pointer (e.g. left over from a version that didn't null it, or a file removed outside the app) never shows as "kept" in the UI.

Applying updates after pulling changes

After a git pull, apply the new code with:

cd /opt/pve-cloud-backup
./scripts/apply-update.sh

The script:

  1. backs up data/app.db to data/update-backups/;
  2. stops web/worker services;
  3. creates/updates backend/.venv;
  4. installs backend requirements;
  5. optionally runs backend tests if RUN_TESTS=1;
  6. runs npm ci or npm install;
  7. builds frontend assets into static/;
  8. runs SQLite migrations;
  9. installs systemd units from systemd/;
  10. reloads systemd;
  11. restarts and health-checks services.

Use this stricter variant when practical:

cd /opt/pve-cloud-backup
git pull --ff-only
RUN_TESTS=1 ./scripts/apply-update.sh

If the health check fails, inspect:

journalctl -u pve-cloud-backup-web.service -u pve-cloud-backup-worker.service --no-pager -n 100

Validation commands

CI workflow:

.gitea/workflows/ci.yml

The workflow is for Gitea Actions and runs backend tests, frontend build, and shell script syntax checks. It uses setup-action package caching for pip and npm, keyed from backend/requirements.txt and frontend/package-lock.json.

Backend tests:

cd /opt/pve-cloud-backup/backend
./.venv/bin/pytest

Frontend typecheck/build:

cd /opt/pve-cloud-backup/frontend
npm run build

Service status:

systemctl is-active pve-cloud-backup-web.service pve-cloud-backup-worker.service

API smoke checks:

curl -fsS http://127.0.0.1:8080/api/setup/status
curl -fsS http://127.0.0.1:8080/api/jobs | python3 -m json.tool
curl -fsS http://127.0.0.1:8080/api/backups | python3 -m json.tool

Manual integration test:

INTEGRATION_VMID=<vmid> API_URL=http://127.0.0.1:8080 /opt/pve-cloud-backup/scripts/integration-test.sh

Only run the manual integration test when the user explicitly wants a real Proxmox/rclone backup test.

Git workflow

Default branch:

main

Do not document private deployment remotes, hostnames, or organization names in committed files. Local git remotes belong in .git/config, not project documentation.

Before committing, check:

git status --short --ignored

Confirm ignored runtime files stay ignored:

git check-ignore -v data/app.db logs/worker.log static/index.html frontend/node_modules/.package-lock.json backend/.venv/pyvenv.cfg