docs: README feature 17 (async-job feedback) + CONTEXT for v0.147.x

README gains the async-job feedback section, the TOC entries it was missing
(SMB was also absent), and the three new endpoints. CONTEXT records the slice
and, more usefully, that three of its four versions exist because the cards were
watched against real runs — the failure modes were all invisible to unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
2026-07-19 10:05:07 +02:00
parent 10ca8ac884
commit 86de16f6dd
2 changed files with 98 additions and 1 deletions
+63
View File
@@ -44,6 +44,8 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated
- [Debug Mode](#13-debug-mode)
- [Geo-Restriction](#14-geo-restriction)
- [App-to-App Integrations](#15-app-to-app-integrations)
- [Network File Sharing — SMB](#16-network-file-sharing--smb-megosztás-v01440-r-7-slice-1)
- [Async-job feedback](#17-async-job-feedback-v0147x-feedback-slice-1)
- [Repository Layout](#repository-layout)
- [Configuration](#configuration)
- [REST API](#rest-api)
@@ -2415,6 +2417,64 @@ e-mail. Note the container name is NOT the stack name (`samba` vs `felhom-samba`
---
### 17. Async-job feedback (v0.147.x, feedback slice 1)
Three long operations that used to be silent now report what they are doing. **These are three
targeted cards on the two existing patterns** (the deploy 3-step panel and the storage-init status
poll), NOT a framework — a unified async-job layer is ROADMAP R-45.
**Verification-restore visibility** (`backup/offbox_verify_copies.go`, `web/offbox_handlers.go`,
`templates/backups_restore.html`). The offsite verification-restore flash now names the **full path**
it wrote to, and `/backups/restore` lists existing verification copies (app · size · date · path)
with a per-copy delete.
- `offsiteRestoreRootFor(drivePath)` is **THE** place `backups/offsite-restore` is spelled;
`offboxRestoreScratchDir` builds on it so listing and delete resolve byte-identical paths to what
the restore wrote.
- `ListOffsiteRestoreCopies()` sweeps every candidate drive in the same preference order the restore
path uses to *choose* one, so it can never miss a copy the restore was capable of creating.
- `DeleteOffsiteRestoreCopy(stack)` takes a **stack name, never a path** — the customer cannot hand
the controller a directory to remove. Guarded by `isSafeStackName` plus a containment assertion on
the resolved path. `POST /backup/offbox/verify-copy/delete` additionally requires `confirm=1`, is
double-confirmed in the UI, and refuses while any backup/restore op is running.
**SMB bring-up progress** (`web/samba_ensure_job.go`, `templates/sharing.html`). `/sharing/enable`
and `/sharing/password` no longer run `ReconcileSamba()` inside the POST — they start a detached
single-flight job (the `storage_init_job.go` shape) and the page polls `GET /sharing/status`.
- The opening phase is decided **before** the work starts, from `stacks.SambaImagePresent()`:
`pulling` („képfájl letöltése") when the pinned image is not in local Docker storage, else
`starting`. Afterwards the image is always present, so the distinction is unrecoverable later.
- Terminal success is **probed** via `stacks.SambaRunning()``compose up -d` exits 0 on a
crash-loop. A `nil` reconcile with `UserSet == false` reports `needs_password`, not `running`.
- `/sharing/status` lets live container state win over a stale/absent job, so a page loaded after a
restart still tells the truth.
**Offsite backup progress** (`backup/offbox_progress.go`, `templates/backups_remote.html`).
`RunOffboxBackupWithProgress` (the **manual** trigger only; the nightly `RunOffboxBackup` is
unchanged and stays silent) installs a progress sink, adds `--json` to the app-backup leg and scans
restic's stdout line-by-line through `offboxStreamRunner` — a streaming sibling of the existing
`offboxRunner` seam, injectable for tests. Published on the existing `GET /backup/offbox/status`
under `progress`.
- Restic's status object is only partly usable in practice, and the fallbacks matter more than the
percentage: on an **incremental** run restic transfers no bytes (`bytes_done` is `omitempty`, so
absent) and `percent_done` stays 0 for the whole run; and because restic 0.14 counts a file only
when it **completes**, an app dominated by one large archive freezes the file counters too. The
card therefore degrades: bytes -> files -> current file + elapsed seconds.
- A run is **not** only the per-app loop. `Phase` (`shares`, `retention`) names the post-app stages
and clears app-scoped counters, so the card never shows the last app's finished numbers against
work that is no longer about that app.
**Infra image pins** (`internal/infra`). `infra.Images()` returns every controller-managed infra
image, derived from the existing pinned consts. `felhom-controller --print-infra-images` prints it
(config-free by design — no `controller.yaml`, data dir or settings are touched) so the golden bake
(`felhom-agent configs/build-golden.sh`) can ask the controller image it is about to bake instead of
keeping its own list, which had already drifted. A `go/ast` test fails if a `*Image` const is added
without reaching `Images()`.
---
## Repository Layout
```
@@ -2646,6 +2706,9 @@ All daily jobs use Europe/Budapest timezone. Skip-if-running prevents concurrent
| POST | `/api/stacks/{name}/cross-backup/run` | Trigger cross-drive backup |
| GET | `/api/stacks/{name}/cross-backup/status` | Cross-drive status |
| POST | `/api/backup/cross-drive/run-all` | Run all scheduled cross-drive backups |
| GET | `/backup/offbox/status` | Offsite run status **+ live `progress` for a manual run** (v0.147.x) |
| POST | `/backup/offbox/verify-copy/delete` | Delete ONE verification copy (stack name + `confirm=1`; v0.147.0) |
| GET | `/sharing/status` | SMB bring-up phase + live container state (v0.147.0) |
### Storage