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>
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.servicepve-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
vzdumpdirectly. Usepveshfor 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.
- upload with
- Never delete a local archive unless upload completed and the remote object was verified.
- Mock
pveshandrclonein automated tests. Manual integration testing is limited toscripts/integration-test.shand 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
pveshandrclone.
- Shell wrappers for
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:
queuedpve_runninglocal_readyuploadingremote_readylocal_deletingcompletedfaileddeletinglocal_downloadingdeleted
Important behavior:
- The worker atomically claims queued backups before starting Proxmox work.
pve_runningmust have a real Proxmox UPID before polling task status.- Local archive discovery must ignore
.logand.notes; only real archive suffixes count. - Remote path shape is:
<rclone_remote>:<rclone_remote_path>/<backup_id>/<archive_name>
local_deletingonly unlinks the local archive when thekeep_local_backupssetting is off (the default). When it's on, the archive stays inlocal_backup_diraftercompletedandlocal_pathstays populated.- Deleting a backup (manual delete, job-cascade delete, retention, or crash-recovery resume of a
deletingbackup) always removes both the remote object and any locally-retained archive, regardless ofkeep_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
deletedstate 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_dirviaPOST /api/backups/{id}/download; this is a transientlocal_downloadingstate that resumes on worker restart. Sincelocal_backup_diris 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 affectingstateorremote_path. local_pathis not trusted blindly:GET /api/backupsandGET /api/backups/{id}verify the file still exists on disk and clearlocal_pathin 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:
- backs up
data/app.dbtodata/update-backups/; - stops web/worker services;
- creates/updates
backend/.venv; - installs backend requirements;
- optionally runs backend tests if
RUN_TESTS=1; - runs
npm ciornpm install; - builds frontend assets into
static/; - runs SQLite migrations;
- installs systemd units from
systemd/; - reloads systemd;
- 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