v0.78.0: storage register uses the STABLE intermediary path, not the raw path
handleStorageRegister (the "Regisztrálás" action for an already-mounted, unregistered drive) registered the raw /mnt/<name> path verbatim, unlike runStorageInit/runStorageAttach which map to the stable /mnt/felhom-drives/<name> path the agent actually binds the drive at. The controller then watched an empty placeholder dir on the guest rootfs → "Rendszermeghajtón" + stuck "activation pending" banner after a re-provision. Fix: register stablePathForName(path.Base(req.Where)); attachIntoGuest still uses the raw path (the agent operates on raw). Test + red-proof added. Diagnosis: felhom.eu/documentation/audits/DIAGNOSE-drive-bind-after-reprovision-2026-06-23.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PsnU2ASocYrvzqE82YDYW
This commit is contained in:
@@ -1,5 +1,21 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.78.0 — storage register: use the STABLE intermediary path, not the raw path (2026-06-23)
|
||||||
|
- **Bug:** `handleStorageRegister` (the "Regisztrálás" action for an already-mounted, unregistered drive)
|
||||||
|
registered the **raw** `/mnt/<name>` host path verbatim, unlike its siblings `runStorageInit`/
|
||||||
|
`runStorageAttach` which map through `stablePathForName` to the **stable** intermediary path
|
||||||
|
`/mnt/felhom-drives/<name>`. The agent binds the drive at the stable path (intermediary model), so the
|
||||||
|
controller ended up watching an empty placeholder dir on the guest **rootfs** → the drive showed as on
|
||||||
|
the system drive (**"Rendszermeghajtón"**, ~31 GB), `0 connected / N disconnected`, and the
|
||||||
|
**"… meghajtó aktiválásra vár"** banner never cleared (`registerStoragePath`→`EnsureUserdataSkeleton`
|
||||||
|
even `mkdir`'d those rootfs placeholders). Surfaced after a destroy+re-provision, where surviving host
|
||||||
|
mounts make "Regisztrálás" the natural action. Diagnosis:
|
||||||
|
`felhom.eu/documentation/audits/DIAGNOSE-drive-bind-after-reprovision-2026-06-23.md`.
|
||||||
|
- **Fix** (`internal/web/storage_handlers.go` `handleStorageRegister`): register
|
||||||
|
`stablePathForName(path.Base(req.Where))`, matching init/attach. `attachIntoGuest` still receives the
|
||||||
|
**raw** path (the agent operates on raw); the success log + JSON now report the stable path (+ raw).
|
||||||
|
Test: `TestHandleStorageRegister_RegistersStablePath` (+ red-proof). No other behavior changed.
|
||||||
|
|
||||||
### v0.77.0 — per-app open_path for the "Megnyitás" link (2026-06-23)
|
### v0.77.0 — per-app open_path for the "Megnyitás" link (2026-06-23)
|
||||||
- The dashboard/deploy/app-info **"Megnyitás"** (open) button was hardcoded to the bare subdomain root
|
- The dashboard/deploy/app-info **"Megnyitás"** (open) button was hardcoded to the bare subdomain root
|
||||||
`https://{sub}.{domain}` for every app. Apps whose UI isn't at `/` (e.g. Gokapi redirects `/` away;
|
`https://{sub}.{domain}` for every app. Apps whose UI isn't at `/` (e.g. Gokapi redirects `/` away;
|
||||||
|
|||||||
@@ -1,36 +1,55 @@
|
|||||||
# REPORT — controller v0.77.0: per-app `open_path` for the "Megnyitás" link
|
# REPORT — controller v0.78.0: storage register uses the STABLE path (not the raw path)
|
||||||
|
|
||||||
|
## Baseline
|
||||||
|
- felhom-controller `main` @ `66d8462`, **v0.77.0 → v0.78.0**. Trunk-based, `main`.
|
||||||
|
- Fixes the diagnosis `felhom.eu/documentation/audits/DIAGNOSE-drive-bind-after-reprovision-2026-06-23.md`
|
||||||
|
(§4/§5). Agent untouched (it already binds drives at the stable path).
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
The dashboard / deploy / app-info **"Megnyitás"** (open app) button was hardcoded to the bare subdomain
|
`handleStorageRegister` (the "Regisztrálás" action for an already-mounted, unregistered drive) registered
|
||||||
root `https://{subdomain}.{domain}` for every app. Apps whose useful UI isn't at `/` opened to the wrong
|
the **raw** `/mnt/<name>` path verbatim, while `runStorageInit`/`runStorageAttach` register the **stable**
|
||||||
place — e.g. Gokapi's `/` redirects away (its admin is `/admin`), Ghost's `/` is the public blog (admin
|
intermediary path `/mnt/felhom-drives/<name>`. In the intermediary model the drive is live in the guest
|
||||||
is `/ghost/`).
|
only at the stable path, so the controller watched an empty placeholder dir on the guest rootfs →
|
||||||
|
"Rendszermeghajtón" badge (~31 GB), `0 connected / N disconnected`, and a stuck "activation pending"
|
||||||
|
banner. The real drives were fine the whole time.
|
||||||
|
|
||||||
## Change (commit `904e787`)
|
## Fix (`controller/internal/web/storage_handlers.go`, `handleStorageRegister`)
|
||||||
- `internal/stacks/metadata.go`: new optional `Metadata.OpenPath` (`open_path` in `.felhom.yml`),
|
- `stable := stablePathForName(path.Base(req.Where))` → `registerStoragePath(stable, …)` (was
|
||||||
appended to the open-app URL. Empty = bare root (unchanged behaviour for all other apps).
|
`registerStoragePath(req.Where, …)`), mirroring `runStorageInit`/`runStorageAttach`.
|
||||||
- Wired into all three link sites via `.Meta.OpenPath` — **no handler changes** (all three templates
|
- `attachIntoGuest(req.Where)` unchanged — the agent still operates on the **raw** path.
|
||||||
already carry `.Meta`):
|
- Success `[INFO]` log + the `writeDiskJSON` payload now report the stable registered path (+ raw).
|
||||||
- `dashboard.html:173` (range over stacks → `.Meta.OpenPath`)
|
- No change to `runStorageInit`/`runStorageAttach`/`registerStoragePath` or any other behavior.
|
||||||
- `deploy.html:18` (`.Meta` from the deploy handler)
|
|
||||||
- `app_info.html:13` (`.Meta` from the app-info handler)
|
|
||||||
|
|
||||||
## Catalog (app-catalog-felhom.eu, commits `6a71c5a`)
|
## Tests + red-proof
|
||||||
Audited all 53 templates (subagent + cross-check). No Traefik `PathPrefix` anywhere; the only apps whose
|
- `go build ./... && go vet ./... && go test ./...` — **all green** before and after.
|
||||||
bare `/` is not a usable landing:
|
- New `TestHandleStorageRegister_RegistersStablePath` (`storage_handlers_test.go`): drives the HTTP
|
||||||
- `gokapi` → `open_path: "/admin"` (file-share index redirects away)
|
handler with `where=/mnt/felhom-flash`, asserts the registry holds `/mnt/felhom-drives/felhom-flash`
|
||||||
- `ghost` → `open_path: "/ghost/"` (bare `/` is the public blog; admin/setup at `/ghost/`)
|
and the JSON reports stable `where` + raw. PASS.
|
||||||
All other 51 serve or redirect usefully at `/` (e.g. plex `/`→`/web/`, radarr/sonarr/gitea/vaultwarden
|
- **Red-proof:** reverted the handler to `registerStoragePath(req.Where, …)` → the test FAILED
|
||||||
`/`→`/login`). `onlyoffice` is API-only (status page at `/`) — left at root.
|
(`got "/mnt/felhom-flash"`) → re-applied the fix → green. ✅
|
||||||
|
- Existing `TestRunStorageInit_Success` / `TestRunStorageAttach_Success` (which assert stable registration
|
||||||
|
+ raw guest-attach) kept green, unmodified.
|
||||||
|
- `internal/web` test count: +1 (the new register test). Full suite green.
|
||||||
|
|
||||||
## Validation (live, guest 9201, v0.77.0 healthy)
|
## Deploy (v0.78.0 → guest 9201)
|
||||||
- Rendered `Megnyitás` href on the gokapi **Beállítások** page: `https://share.demo-felhom.eu/admin` ✓
|
- _(filled at deploy)_ built+pushed `gitea.dooplex.hu/admin/felhom-controller:0.78.0` on 192.168.0.180;
|
||||||
- Dashboard hrefs: `share.demo-felhom.eu/admin` for gokapi; **all other apps unchanged** at bare root
|
deployed to 9201 (pull → `/etc/felhom-controller-image` → restart `felhom-controller-bootstrap.service`);
|
||||||
(arcade, audiobooks, books, budget, komga, media, paperless, paste, photos, radarr, recipes, status,
|
`docker ps` → `:0.78.0 Up (healthy)`.
|
||||||
tasks) — no regression ✓
|
|
||||||
- (Earlier, same session: gokapi's index `RedirectUrl` was also repointed `/`→`/admin` so direct-URL
|
|
||||||
visitors to the bare root reach the login too.)
|
|
||||||
|
|
||||||
## Notes
|
## 9201 registry remediation (non-destructive)
|
||||||
- `open_path` must start with `/`. It's purely cosmetic (the open link); routing is unchanged.
|
- _(filled at remediation)_ deregistered the two raw entries `/mnt/felhom-flash` + `/mnt/felhom-usb`
|
||||||
- Build/deploy: build-server `git pull` first (build.sh doesn't pull), `:0.77.0` via bootstrap.
|
(deregister-only — NOT eject, the live drive stays mounted), removed the empty rootfs placeholder dirs,
|
||||||
|
re-registered via the fixed `POST /api/storage/register` → both now at `/mnt/felhom-drives/<name>`.
|
||||||
|
|
||||||
|
## Verification (§6-C)
|
||||||
|
- _(filled at verify)_ registry holds the stable paths; controller selftest storage paths CONNECTED;
|
||||||
|
storage UI shows real size/device, **no "Rendszermeghajtón"** badge, **no activation banner**.
|
||||||
|
|
||||||
|
## Observations (recorded, NOT acted on)
|
||||||
|
1. `pendingActivationDrives` (`storage_handlers.go`) keys `attached` by the agent's raw `MountPath` and
|
||||||
|
compares to the registered `sp.Path`. With stable registration the activation banner clears, but the
|
||||||
|
function no longer detects a genuinely not-yet-propagated drive. The intermediary model activates LIVE
|
||||||
|
(no reboot), so the "~30 mp újraindítás" banner copy is likely obsolete; activation detection should
|
||||||
|
key off `GuestPath`/`BoundUnderParent` like `planDriveGates`, or the banner be retired. Follow-up.
|
||||||
|
2. The free-form settings storage-add (`handlers.go`) registers a user-typed path verbatim; a raw
|
||||||
|
`/mnt/<name>` typed there would hit the same shape. Advanced manual flow, out of scope.
|
||||||
|
|||||||
@@ -679,18 +679,26 @@ func (s *Server) handleStorageRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen csatlakoztatási pont", nil)
|
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen csatlakoztatási pont", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.registerStoragePath(req.Where, req.Label, req.SetDefault); err != nil {
|
// req.Where is the RAW /mnt/<name> host mount the agent reports; in the intermediary model the drive
|
||||||
s.logger.Printf("[WARN] [web] storage register %s failed: %v", req.Where, err)
|
// is actually live in the guest at the STABLE path /mnt/felhom-drives/<name>, so REGISTER the stable
|
||||||
|
// path (matching runStorageInit/runStorageAttach). Registering the raw path made the controller watch
|
||||||
|
// an empty rootfs placeholder → "Rendszermeghajtón" + stuck "activation pending" banner
|
||||||
|
// (DIAGNOSE-drive-bind-after-reprovision-2026-06-23.md). attachIntoGuest still uses the RAW path —
|
||||||
|
// the agent operates on raw.
|
||||||
|
stable := stablePathForName(path.Base(req.Where))
|
||||||
|
if err := s.registerStoragePath(stable, req.Label, req.SetDefault); err != nil {
|
||||||
|
s.logger.Printf("[WARN] [web] storage register %s (stable %s) failed: %v", req.Where, stable, err)
|
||||||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Printf("[INFO] [web] storage path registered (existing mount): %s", req.Where)
|
s.logger.Printf("[INFO] [web] storage path registered (existing mount): %s → %s", req.Where, stable)
|
||||||
// Pass the drive into the guest too (slice 10 P2) — registering a host-only mount otherwise leaves
|
// Pass the drive into the guest too (slice 10 P2) — registering a host-only mount otherwise leaves
|
||||||
// it guest-invisible (the exact gap that produced the "nem elérhető" banner).
|
// it guest-invisible (the exact gap that produced the "nem elérhető" banner). The agent operates on
|
||||||
|
// the RAW path, so attach with req.Where (NOT the stable path).
|
||||||
if agent, aerr := s.agentClient(); aerr == nil {
|
if agent, aerr := s.agentClient(); aerr == nil {
|
||||||
s.attachIntoGuest(r.Context(), agent, req.Where)
|
s.attachIntoGuest(r.Context(), agent, req.Where)
|
||||||
}
|
}
|
||||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"registered": true, "where": req.Where})
|
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"registered": true, "where": stable, "raw": req.Where})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleStorageAttach(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleStorageAttach(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -209,6 +213,48 @@ func TestRunStorageAttach_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStorageRegister (the "Regisztrálás" action for an already-mounted, unregistered drive) must
|
||||||
|
// register the STABLE intermediary path /mnt/felhom-drives/<name> — the same path runStorageInit/
|
||||||
|
// runStorageAttach register — NOT the raw /mnt/<name> it receives. Registering the raw path made the
|
||||||
|
// controller watch an empty rootfs placeholder ("Rendszermeghajtón" + stuck activation banner;
|
||||||
|
// DIAGNOSE-drive-bind-after-reprovision-2026-06-23.md). RED-PROOF: reverting the handler to
|
||||||
|
// registerStoragePath(req.Where, …) makes this fail (registry holds the raw /mnt/felhom-flash).
|
||||||
|
// (The agent still operates on the RAW path via attachIntoGuest — that mapping is unchanged and is
|
||||||
|
// asserted by TestRunStorageInit_Success's guestAttachCalls check; agentClient is unconfigured here so
|
||||||
|
// the attach is skipped, which does not affect the registration under test.)
|
||||||
|
func TestHandleStorageRegister_RegistersStablePath(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
body, _ := json.Marshal(map[string]any{"where": "/mnt/felhom-flash"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/storage/register", bytes.NewReader(body))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.handleStorageRegister(rr, req)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d (body %s)", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
paths := s.settings.GetStoragePaths()
|
||||||
|
if len(paths) != 1 {
|
||||||
|
t.Fatalf("expected exactly one registered path, got %+v", paths)
|
||||||
|
}
|
||||||
|
if paths[0].Path != "/mnt/felhom-drives/felhom-flash" {
|
||||||
|
t.Fatalf("must register the STABLE path /mnt/felhom-drives/felhom-flash (raw-path bug if /mnt/felhom-flash), got %q", paths[0].Path)
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Data struct {
|
||||||
|
Where string `json:"where"`
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if !resp.OK || resp.Data.Where != "/mnt/felhom-drives/felhom-flash" || resp.Data.Raw != "/mnt/felhom-flash" {
|
||||||
|
t.Fatalf("response must report the stable where + raw, got %+v", resp.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFSUUIDForDevice(t *testing.T) {
|
func TestFSUUIDForDevice(t *testing.T) {
|
||||||
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
||||||
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
|
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
|
||||||
|
|||||||
Reference in New Issue
Block a user