D53 built + live-proven (08 §10 [DESIGN — built]); R-1297 decoys: register-shape decoys assert the script's exit
gates / gates (push) Successful in 3m25s

- 08 §10: built (controller 5bd03ad, hub 7e57e824, commit only); putback_pending as on the wire; as-built notes
- audits/d53-held-apps-2026-10-26: live proof on 9202 against a test hub (3-min grace), red-proofs, teardown
- R-1291 BUILT (not released); R-1298 filed (9202's orphan holds + PrivateBin leftover)
- test_gate_decoys.py: the register-shape cases also run the script and assert its exit code — R-1297's decoy 2
  showed an inert main() passed every case (9 cases red with it now)
- audits/r1297-ci-trim-2026-10-26: runners, times, decoys
This commit is contained in:
admin committed 2026-10-26 19:03:07 +01:00
1 parent 7e57e824a2
commit e33ec8210a
23 files changed
+2464 -7

No files matched your search

+16 -5
View File
@@ -610,7 +610,7 @@ normal use; work it does not explain is the sign of a hacked app. Numbers and me
(Samba) has no counter; the push floor is a lower bound, the weekly test is close to the line.
7. **Release:** released 2026-10-24 on the operator's yes (controller v0.342.0, hub v0.169.0; R-1244) — corrected 2026-10-24.
## 10. A held app the operator can see [DESIGN — proposed, R-1291, 2026-10-26; no code]
## 10. A held app the operator can see [DESIGN — built 2026-10-26 (commit only): controller `5bd03ad`, hub `7e57e824`; D53 = operator-only, `09` §3 decision 265; R-1291]
**Why.** A held app is stopped on purpose and stays stopped until a person acts — and today nobody is told after the
first minute. demo-hp's Kimai sat held for 11 days (2026-10-14 → 10-26) and was found by accident (R-1286). The
@@ -625,7 +625,7 @@ anything, and the hub has no held-app state at all. On a household box the same
| `restore_mixed` | an off-site restore whose database replay failed | a person (the same) |
| `update_failed` | a failed update | the household, by restoring the app from its backup |
| `unhealthy_stop` | the crash-loop guard | the household's Start (one more try, `09` §3 decision 28) |
| `put_back_pending` | a restore waiting for its pre-restore copy | nobody — it clears itself when the drive comes back |
| `putback_pending` | a restore waiting for its pre-restore copy | nobody — it clears itself when the drive comes back |
**What the controller reports.** The legacy report is frozen (hub rule), so the event channel carries it:
`app_held` when a hold is written (stack, reason, since — the hold's own `At`) and `app_hold_cleared` when it goes.
@@ -636,7 +636,7 @@ treats it as idempotent. Both types enter `allowedEventTypes` AND `customerMessa
**Where the hub shows it.** The host page (and the customer page) get a „Held apps" list: app, reason in one line
(the `holdWhy` sentence), since, and who may clear it. Built from the open `app_held` events per customer and stack.
**When it mails.** One operator mail per hold after a grace that follows the reason: `put_back_pending` 6 hours (it
**When it mails.** One operator mail per hold after a grace that follows the reason: `putback_pending` 6 hours (it
should have cleared itself — if not, something is stuck); `restore_failed` / `restore_mixed` 1 day (only a person can
act); `update_failed` and `unhealthy_stop` 3 days (the household can act first). Then one reminder a week while it
lasts, through the dispatcher's existing per-type cooldown (§6.2). A cleared hold sends nothing.
@@ -647,5 +647,16 @@ never clears a hold** — it only shows and reminds.
**Cost.** One controller release (the two events + the daily re-send, ~½ day with tests) and one hub release (allow-list,
the i18n keys, the list on two pages, a reminder checker; ~1 day). No agent change.
**Open pick (STATUS D53):** v1 operator-only (CC's pick), or also mail the household for the two holds it can clear
itself („press Start" / „restore from the backup")?
**Ruled (D53, operator 2026-10-26, `09` §3 decision 265):** v1 is operator-only. Household mails for the two holds it can
clear itself may follow once the operator list has run.
**As built (2026-10-26, commit only — releases ride an attended run).** The wire reason is the controller's own constant:
`putback_pending` (one word), and the empty legacy reason is sent as `restore_failed`. The controller's reporter is ONE
observer of the hold store, ticked every minute (`internal/holdreport`), never a send inside a hold writer; the set it
last reported is kept in settings, so a hold cleared while the controller was down is still reported cleared. The hub
mirrors the events in `held_apps`, sends `app_held_reminder` (warning, operator-only, per-app cooldown, the subject
names the app) once after the grace and then weekly, records the reminder before it sends it, and does not remind about
a row the box stopped repeating for 50 h (the list still shows it, marked). `alerting.held_app_grace_override` replaces
every grace — a test configuration only. Live proof: `audits/d53-held-apps-2026-10-26/` (a crash-looped throwaway app on
9202 against a test hub with a 3-minute grace: listed on both pages, one operator mail, Start cleared it, the row went,
every household leg `skipped operator_only`).
@@ -0,0 +1,62 @@
# D53 — a held app reaches the operator: build and live proof (2026-10-26 evening)
**Design:** `architecture/08-alarm-ladder.md` §10. **Ruling:** D53 = operator-only reminders first (`09` §3 decision 265).
**Code (commit only, not released):** controller `5bd03ad` (CI 2377 ✓), hub `7e57e824` (CI 2378 ✓).
## Red proofs
- Controller `internal/holdreport`: with an inert `Tick`, all six behaviour tests failed; with the scheduler line
commented out, the wiring test failed.
- Hub: six mutations, each turned its test red — `redproof-hub.txt` (one first attempt was only a compile error; it was
redone with a compiling mutation, the last line).
## Live proof — method
No hub deploy is allowed in this run, so the hub side ran as a **test hub process** on DooPlex (`192.168.0.180:18090`),
built from a clean clone at `7e57e824`, scratch database, a throwaway login, its own random global key, operator mail to
`d53-test@felhom.eu` (the catch-all), and `alerting.held_app_grace_override: 3m` (the test configuration). Scratch guest
9202 ran a **local-only** controller image built from `5bd03ad` (`0.346.0-d53test`, `docker save`/`load`, never pushed to
the registry), pointed at the test hub for the test. Pages were read through the test hub's own login (the endpoints
the operator's browser uses); the mail was read in the mailbox (a different channel from the hub log).
## Live proof — what happened (UTC)
| Time | What | Evidence |
|---|---|---|
| 17:28 | Both lists empty: „No app is held on this box." / „… on any box." | `live/0-before_*.html` |
| 17:29:52 | Test controller up; „Notifier enabled (hub: …:18090)", job `hold-report` every 1m | `live/9202-controller-log.txt` |
| 17:30:52 | The first tick reported **four old holds 9202 has carried since 2026-09-24** (`chaoscrash`, `chaosoom`, `chaosoomb`, `gokapi`) | `live/test-hub.log` |
| 17:33:35 | Four operator reminders (one per app, per-app cooldown) | mailbox, `live/notification_log.txt` |
| 17:35:41 | Throwaway Radicale installed through the product | — |
| 17:36:18–17:37:48 | Seven `kill -9` of its main process, 15 s apart | — |
| 17:37:52 | The guard stopped it: „crash_loop — 7 in 10m0s; STOPPING it"; hold `unhealthy_stop` | `live/9202-controller-log.txt` |
| 17:38:52 | `app_held` radicale reached the hub (next tick) | `live/test-hub.log` |
| 17:41:34 | One reminder after the 3-minute grace; mail „[Felhom] ⚠️ demo-hp: the app radicale is still held — a person must act" in the inbox at 17:41:35 | mailbox |
| 17:42 | Both pages list 5 held apps, Radicale with its reason, sentence, and who may clear it | `live/1-held_*.html` |
| 17:45:37 | The household's Start (`POST /api/stacks/radicale/start`) lifted the hold | controller log |
| 17:45:52 | `app_hold_cleared` → the Radicale row went from both pages; no reminder after it | `live/2-cleared_*.html`, `live/held_apps_final.txt` |
**No household mail:** every reminder's customer leg is `skipped operator_only` (`live/notification_log.txt`).
## Teardown
- **Machine (9202):** Radicale removed through the product (no container, volume, `app.yaml`, data or subdomain left);
config and image file put back byte-identical to the saved copies; the test's two settings traces (`reported_holds`, the
Radicale stop time) removed while the controller was stopped; the test image removed; controller 0.343.0 healthy, hub off,
exactly as found. The four old holds and an old PrivateBin record (below) were there before and stay.
- **Host (demo-hp):** nothing provisioned; the transfer file removed.
- **Hub:** the real hub was not touched. The test hub process stopped; its database, login, key and binary deleted. The
local test image on DooPlex removed.
## Observations
- FILED: R-1298 — 9202 carries leftovers made outside the product: four crash-loop holds since 2026-09-24 for apps that
are not installed, and a PrivateBin `app.yaml` (2026-10-25) with no container, so the product reads it as installed.
Once D53 is released, such orphan holds would be reminded weekly about apps that do not exist.
- NOT-A-FINDING: the test hub's log looked cut mid-line at 18:45:52 local — the `sec` wrapper holds back an unfinished
part of a long-running child's output; the full line was there when the process ended.
- NOT-A-FINDING: my first switch of 9202 used `systemctl start` on the bootstrap oneshot, which was still active, so the
controller was briefly not running (about one minute); `restart` is the right verb. Fixed in the run.
Secrets: the evidence was scanned for the Resend key, the hub token, the report key and 9202's password — 0 hits; the
test hub's CSRF tokens are redacted.
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hosts — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<script>
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-sm btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-sm btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
</script>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<h2 style="margin-bottom: 1rem;">Hosts</h2>
<section class="card" id="held-apps" style="margin-bottom: 1.5rem; padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem; margin-top: 0;">Held apps</h2>
<div class="empty-state" style="border: none;">
<p>No app is held on any box.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<table class="data-table">
<thead>
<tr>
<th>Host</th>
<th>Customer</th>
<th>Agent</th>
<th>Proxmox / kernel</th>
<th>Status</th>
<th>Guests</th>
<th>CPU</th>
<th>Mem</th>
<th>Disk</th>
<th>Cloudflared</th>
<th>Worst Storage</th>
</tr>
</thead>
<tbody>
<tr onclick="window.location='/hosts/d53-test-9202'" style="cursor: pointer;">
<td><a href="/hosts/d53-test-9202">d53-test-9202</a></td>
<td>demo-hp</td>
<td>—</td>
<td style="font-size: 0.85em;">unknown<br><span class="text-muted">unknown</span></td>
<td><span class="status-badge status-badge-pending">NO REPORT</span></td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
</section>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>d53-test-9202 — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<a href="/hosts" class="back-link">&larr; Hosts</a>
<section class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h2 style="margin: 0;">d53-test-9202</h2>
<span class="status-badge status-badge-pending">NO REPORT</span>
</div>
<div class="info-grid" style="margin-top: 1rem;">
<div class="info-item">
<span class="label">Host ID</span>
<span class="value" style="font-family: var(--font-mono)">d53-test-9202</span>
</div>
<div class="info-item">
<span class="label">Customer</span>
<span class="value"><a href="/customers/demo-hp">demo-hp</a></span>
</div>
<div class="info-item">
<span class="label">Agent Version</span>
<span class="value">—</span>
</div>
<div class="info-item">
<span class="label">Enrolled</span>
<span class="value">just now</span>
</div>
<div class="info-item">
<span class="label">Last Report</span>
<span class="value">waiting for first report</span>
</div>
<div class="info-item">
<span class="label">Box connection</span>
<span class="value" data-box-connection="unknown">unknown (the hub restarted recently, or the host has no customer)</span>
</div>
<div class="info-item">
<span class="label">Desired Generation</span>
<span class="value">0</span>
</div>
</div>
</section>
<section class="card">
<div class="empty-state">
<p>Waiting for first report.</p>
<p class="hint">Vitals, guests and storage appear once this host's agent sends a host-report.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Guests</h2>
<div class="empty-state" style="border: none;">
<p>No guests reported on this host.</p>
</div>
</section>
<section class="card" id="held-apps" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Held apps</h2>
<div class="empty-state" style="border: none;">
<p>No app is held on this box.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Storage Targets</h2>
<div class="empty-state" style="border: none;">
<p>No storage targets reported.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Capabilities</h2>
<div class="empty-state" style="border: none;">
<p>No capability report yet (waiting for the first host report).</p>
</div>
</section>
<section class="card">
<h2>Diagnostics — Log Bundles</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
Pull-based: the box ships its debug ring on its own next cycle — controller &le; one report interval (~15 min),
agent &asymp; one heartbeat. The pull is recorded in the box's own log (customer-visible). Bundles expire after 72 h.
</p>
<div style="display: flex; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="controller">
<button type="submit" class="btn btn-sm">Request controller logs</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="agent">
<button type="submit" class="btn btn-sm">Request agent logs</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No log bundles. Use the request buttons above — the box delivers on its next cycle.</p>
</div>
</section>
<section class="card" id="operator-actions">
<h2>Operator Actions</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
The box acts on its next report reply (usually seconds, at most one report interval) and answers on the
report after that. Each press runs once. Recorded in the box's own log and in Events. Unanswered after a day: expired.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="offsite_backup_now">
<button type="submit" class="btn btn-sm">Run off-site backup now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="run_job">
<select name="arg"><option value="disk-health-check">disk-health-check</option><option value="fill-watch">fill-watch</option><option value="offsite-integrity">offsite-integrity</option><option value="offsite-proof">offsite-proof</option></select>
<button type="submit" class="btn btn-sm">Run check now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_stop">
<button type="submit" class="btn btn-sm btn-outline">Stop deletion countdown</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_extend">
<input type="number" name="arg" min="1" max="30" value="7" style="width: 4.5em; padding: 0.3em 0.5em;" aria-label="days">
<button type="submit" class="btn btn-sm btn-outline">Extend countdown (days from now)</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No operator actions for this box yet.</p>
</div>
</section>
<section class="card">
<h2>Network</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">WireGuard</span>
<span class="value">
<span class="text-muted">no peer allocated</span>
</span>
</div>
</div>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
Waiting for the first host report.
</p>
</section>
<section class="card">
<h2>Guest network
<span class="badge badge-neutral" title="A capable agent sent no guest-network stanza">unknown</span>
</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
This host reported no guest-network state, so it is <strong>unknown</strong> &mdash; not healthy.
The watchdog is default-on; a silent capable agent means it was switched off
(<code>guest_net.disable</code>), or this report predates it on this box.
</p>
</section>
<section class="card">
<h2>DR / Backup</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">DR Recipe</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item">
<span class="label">Key Escrow</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item" id="offsite-integrity">
<span class="label">Last full check (off-site)</span>
<span class="value">not reported (no off-site status in the box&#39;s report)</span>
</div>
</div>
</section>
<section class="card">
<h2>Console access</h2>
<p><span class="badge badge-neutral">not vaulted</span></p>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
No console credential is vaulted for this host. Expected for a byo host — the owner manages root@pam. Otherwise the installer's step 4b did not run; re-run felhom-host-install.sh, or set and vault one per the break-glass runbook &sect;5.
</p>
</section>
<section class="card" style="border-color: var(--crit);">
<h2>Danger zone</h2>
<p class="text-muted" style="font-size: 0.85rem;">
Removing this host deletes its reports, guests, log bundles and WireGuard peer, and
permanently invalidates its API key — a still-running agent would receive 401s.
Re-enrollment requires the Day-0 passphrase flow.
</p>
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteConfirm('d53-test-9202')">Remove host&hellip;</button>
<div id="host-delete-confirm-d53-test-9202" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); background: var(--crit-dim); border-radius: var(--radius); max-width: 44em;">
<p id="host-delete-impact-d53-test-9202" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
<label id="host-delete-escrow-row-d53-test-9202" style="display: none; margin: 0 0 0.5rem; font-size: 0.85em;">
<input type="checkbox" id="host-delete-escrow-d53-test-9202">
Move key escrow to retained custody (required when escrow present) + remove DR bundle for this host
</label>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: var(--text-2);">Type the host id to confirm:</p>
<form method="POST" action="/hosts/d53-test-9202/delete" id="host-delete-form-d53-test-9202" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="confirm_host_id" id="host-delete-confirm-hidden-d53-test-9202" value="">
<input type="hidden" name="delete_escrow" id="host-delete-escrow-hidden-d53-test-9202" value="">
<input type="text" id="host-delete-confirm-input-d53-test-9202" placeholder="retype the host id&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteSubmit('d53-test-9202')">Confirm &amp; remove</button>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('host-delete-confirm-d53-test-9202').style.display='none';">Cancel</button>
</form>
<p id="host-delete-err-d53-test-9202" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
</div>
</section>
<script>
function hostDeleteConfirm(hostID) {
var box = document.getElementById('host-delete-confirm-' + hostID);
var impact = document.getElementById('host-delete-impact-' + hostID);
document.getElementById('host-delete-confirm-input-' + hostID).value = '';
document.getElementById('host-delete-err-' + hostID).textContent = '';
box.style.display = 'block';
impact.textContent = 'Checking impact…';
fetch('/hosts/' + encodeURIComponent(hostID) + '/delete-impact')
.then(function(r){ return r.json(); })
.then(function(d){
var parts = ['Deleting ' + hostID + ' removes ' + d.guests + ' guest row(s), ' +
d.reports + ' host report(s), ' + d.log_bundles + ' agent log bundle(s)'];
if (d.wg_peer_bound) parts.push('the bound WireGuard peer');
if (d.pbs_secret_present) parts.push('the staged PBS secret');
if (d.recovery_present) parts.push('the break-glass recovery credential');
impact.textContent = parts.join(', ') + '. Host status: ' + d.status + '.' +
(d.deletable ? (d.off_tick_required ? ' No box connection for 6+ minutes — the "box is off" tick is required.' : '') : ' Host is ONLINE — deletion will be refused.');
document.getElementById('host-delete-escrow-row-' + hostID).style.display =
d.escrow_present ? 'block' : 'none';
})
.catch(function(){ impact.textContent = 'Could not compute the impact — the server will still enforce every gate.'; });
}
function hostDeleteSubmit(hostID) {
var typed = document.getElementById('host-delete-confirm-input-' + hostID).value.trim();
var err = document.getElementById('host-delete-err-' + hostID);
if (typed !== hostID) { err.textContent = 'Confirmation does not match the host id.'; return; }
var escrowRow = document.getElementById('host-delete-escrow-row-' + hostID);
var escrowCb = document.getElementById('host-delete-escrow-' + hostID);
if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
err.textContent = 'This host has a key escrow — tick the acknowledgement to delete it too.';
return;
}
var offCb = document.getElementById('host-delete-off-' + hostID);
if (offCb && !offCb.checked) {
err.textContent = 'Tick "I checked: the box is off" first.';
return;
}
if (offCb) { document.getElementById('host-delete-off-hidden-' + hostID).value = '1'; }
document.getElementById('host-delete-confirm-hidden-' + hostID).value = typed;
document.getElementById('host-delete-escrow-hidden-' + hostID).value = escrowCb.checked ? '1' : '';
document.getElementById('host-delete-form-' + hostID).submit();
}
</script>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hosts — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<script>
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-sm btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-sm btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
</script>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<h2 style="margin-bottom: 1rem;">Hosts</h2>
<section class="card" id="held-apps" style="margin-bottom: 1.5rem; padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem; margin-top: 0;">Held apps</h2>
<table class="data-table">
<thead>
<tr>
<th>Customer</th>
<th>App</th>
<th>Reason</th>
<th>Since</th>
<th>Who may clear it</th>
</tr>
</thead>
<tbody>
<tr data-held-app="demo-hp/gokapi">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>gokapi</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaosoom">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaosoom</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaoscrash">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaoscrash</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaosoomb">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaosoomb</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/radicale">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>radicale</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>7 min ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
</tbody>
</table>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<table class="data-table">
<thead>
<tr>
<th>Host</th>
<th>Customer</th>
<th>Agent</th>
<th>Proxmox / kernel</th>
<th>Status</th>
<th>Guests</th>
<th>CPU</th>
<th>Mem</th>
<th>Disk</th>
<th>Cloudflared</th>
<th>Worst Storage</th>
</tr>
</thead>
<tbody>
<tr onclick="window.location='/hosts/d53-test-9202'" style="cursor: pointer;">
<td><a href="/hosts/d53-test-9202">d53-test-9202</a></td>
<td>Demo HP (scratch guest 9202)</td>
<td>—</td>
<td style="font-size: 0.85em;">unknown<br><span class="text-muted">unknown</span></td>
<td><span class="status-badge status-badge-pending">NO REPORT</span></td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
</section>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,399 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>d53-test-9202 — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<a href="/hosts" class="back-link">&larr; Hosts</a>
<section class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h2 style="margin: 0;">d53-test-9202</h2>
<span class="status-badge status-badge-pending">NO REPORT</span>
</div>
<div class="info-grid" style="margin-top: 1rem;">
<div class="info-item">
<span class="label">Host ID</span>
<span class="value" style="font-family: var(--font-mono)">d53-test-9202</span>
</div>
<div class="info-item">
<span class="label">Customer</span>
<span class="value"><a href="/customers/demo-hp">Demo HP (scratch guest 9202)</a></span>
</div>
<div class="info-item">
<span class="label">Agent Version</span>
<span class="value">—</span>
</div>
<div class="info-item">
<span class="label">Enrolled</span>
<span class="value">16 min ago</span>
</div>
<div class="info-item">
<span class="label">Last Report</span>
<span class="value">waiting for first report</span>
</div>
<div class="info-item">
<span class="label">Box connection</span>
<span class="value" data-box-connection="not_connected" style="color: var(--warn)">not connected since the hub started (2026-10-26 17:28 UTC, 17 min ago)</span>
</div>
<div class="info-item">
<span class="label">Desired Generation</span>
<span class="value">0</span>
</div>
</div>
</section>
<section class="card">
<div class="empty-state">
<p>Waiting for first report.</p>
<p class="hint">Vitals, guests and storage appear once this host's agent sends a host-report.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Guests</h2>
<div class="empty-state" style="border: none;">
<p>No guests reported on this host.</p>
</div>
</section>
<section class="card" id="held-apps" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Held apps</h2>
<table class="data-table">
<thead>
<tr>
<th>App</th>
<th>Reason</th>
<th>Since</th>
<th>Why</th>
<th>Who may clear it</th>
</tr>
</thead>
<tbody>
<tr data-held-app="gokapi">
<td><code>gokapi</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a crash loop (2026-09-24T10:37:48Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaosoom">
<td><code>chaosoom</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a oom storm (2026-09-24T11:48:01Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaoscrash">
<td><code>chaoscrash</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a crash loop (2026-09-24T12:16:24Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaosoomb">
<td><code>chaosoomb</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a oom storm (2026-09-24T12:39:51Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="radicale">
<td><code>radicale</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>7 min ago</td>
<td>stopped by the box for a crash loop (2026-10-26T17:37:52Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
</tbody>
</table>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Storage Targets</h2>
<div class="empty-state" style="border: none;">
<p>No storage targets reported.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Capabilities</h2>
<div class="empty-state" style="border: none;">
<p>No capability report yet (waiting for the first host report).</p>
</div>
</section>
<section class="card">
<h2>Diagnostics — Log Bundles</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
Pull-based: the box ships its debug ring on its own next cycle — controller &le; one report interval (~15 min),
agent &asymp; one heartbeat. The pull is recorded in the box's own log (customer-visible). Bundles expire after 72 h.
</p>
<div style="display: flex; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="controller">
<button type="submit" class="btn btn-sm">Request controller logs</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="agent">
<button type="submit" class="btn btn-sm">Request agent logs</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No log bundles. Use the request buttons above — the box delivers on its next cycle.</p>
</div>
</section>
<section class="card" id="operator-actions">
<h2>Operator Actions</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
The box acts on its next report reply (usually seconds, at most one report interval) and answers on the
report after that. Each press runs once. Recorded in the box's own log and in Events. Unanswered after a day: expired.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="offsite_backup_now">
<button type="submit" class="btn btn-sm">Run off-site backup now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="run_job">
<select name="arg"><option value="disk-health-check">disk-health-check</option><option value="fill-watch">fill-watch</option><option value="offsite-integrity">offsite-integrity</option><option value="offsite-proof">offsite-proof</option></select>
<button type="submit" class="btn btn-sm">Run check now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_stop">
<button type="submit" class="btn btn-sm btn-outline">Stop deletion countdown</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_extend">
<input type="number" name="arg" min="1" max="30" value="7" style="width: 4.5em; padding: 0.3em 0.5em;" aria-label="days">
<button type="submit" class="btn btn-sm btn-outline">Extend countdown (days from now)</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No operator actions for this box yet.</p>
</div>
</section>
<section class="card">
<h2>Network</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">WireGuard</span>
<span class="value">
<span class="text-muted">no peer allocated</span>
</span>
</div>
</div>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
Waiting for the first host report.
</p>
</section>
<section class="card">
<h2>Guest network
<span class="badge badge-neutral" title="A capable agent sent no guest-network stanza">unknown</span>
</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
This host reported no guest-network state, so it is <strong>unknown</strong> &mdash; not healthy.
The watchdog is default-on; a silent capable agent means it was switched off
(<code>guest_net.disable</code>), or this report predates it on this box.
</p>
</section>
<section class="card">
<h2>DR / Backup</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">DR Recipe</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item">
<span class="label">Key Escrow</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item" id="offsite-integrity">
<span class="label">Last full check (off-site)</span>
<span class="value">not reported (no off-site status in the box&#39;s report)</span>
</div>
</div>
</section>
<section class="card">
<h2>Console access</h2>
<p><span class="badge badge-neutral">not vaulted</span></p>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
No console credential is vaulted for this host. Expected for a byo host — the owner manages root@pam. Otherwise the installer's step 4b did not run; re-run felhom-host-install.sh, or set and vault one per the break-glass runbook &sect;5.
</p>
</section>
<section class="card" style="border-color: var(--crit);">
<h2>Danger zone</h2>
<p class="text-muted" style="font-size: 0.85rem;">
Removing this host deletes its reports, guests, log bundles and WireGuard peer, and
permanently invalidates its API key — a still-running agent would receive 401s.
Re-enrollment requires the Day-0 passphrase flow.
</p>
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteConfirm('d53-test-9202')">Remove host&hellip;</button>
<div id="host-delete-confirm-d53-test-9202" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); background: var(--crit-dim); border-radius: var(--radius); max-width: 44em;">
<p id="host-delete-impact-d53-test-9202" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
<label id="host-delete-escrow-row-d53-test-9202" style="display: none; margin: 0 0 0.5rem; font-size: 0.85em;">
<input type="checkbox" id="host-delete-escrow-d53-test-9202">
Move key escrow to retained custody (required when escrow present) + remove DR bundle for this host
</label>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: var(--text-2);">Type the host id to confirm:</p>
<form method="POST" action="/hosts/d53-test-9202/delete" id="host-delete-form-d53-test-9202" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="confirm_host_id" id="host-delete-confirm-hidden-d53-test-9202" value="">
<input type="hidden" name="delete_escrow" id="host-delete-escrow-hidden-d53-test-9202" value="">
<input type="text" id="host-delete-confirm-input-d53-test-9202" placeholder="retype the host id&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteSubmit('d53-test-9202')">Confirm &amp; remove</button>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('host-delete-confirm-d53-test-9202').style.display='none';">Cancel</button>
</form>
<p id="host-delete-err-d53-test-9202" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
</div>
</section>
<script>
function hostDeleteConfirm(hostID) {
var box = document.getElementById('host-delete-confirm-' + hostID);
var impact = document.getElementById('host-delete-impact-' + hostID);
document.getElementById('host-delete-confirm-input-' + hostID).value = '';
document.getElementById('host-delete-err-' + hostID).textContent = '';
box.style.display = 'block';
impact.textContent = 'Checking impact…';
fetch('/hosts/' + encodeURIComponent(hostID) + '/delete-impact')
.then(function(r){ return r.json(); })
.then(function(d){
var parts = ['Deleting ' + hostID + ' removes ' + d.guests + ' guest row(s), ' +
d.reports + ' host report(s), ' + d.log_bundles + ' agent log bundle(s)'];
if (d.wg_peer_bound) parts.push('the bound WireGuard peer');
if (d.pbs_secret_present) parts.push('the staged PBS secret');
if (d.recovery_present) parts.push('the break-glass recovery credential');
impact.textContent = parts.join(', ') + '. Host status: ' + d.status + '.' +
(d.deletable ? (d.off_tick_required ? ' No box connection for 6+ minutes — the "box is off" tick is required.' : '') : ' Host is ONLINE — deletion will be refused.');
document.getElementById('host-delete-escrow-row-' + hostID).style.display =
d.escrow_present ? 'block' : 'none';
})
.catch(function(){ impact.textContent = 'Could not compute the impact — the server will still enforce every gate.'; });
}
function hostDeleteSubmit(hostID) {
var typed = document.getElementById('host-delete-confirm-input-' + hostID).value.trim();
var err = document.getElementById('host-delete-err-' + hostID);
if (typed !== hostID) { err.textContent = 'Confirmation does not match the host id.'; return; }
var escrowRow = document.getElementById('host-delete-escrow-row-' + hostID);
var escrowCb = document.getElementById('host-delete-escrow-' + hostID);
if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
err.textContent = 'This host has a key escrow — tick the acknowledgement to delete it too.';
return;
}
var offCb = document.getElementById('host-delete-off-' + hostID);
if (offCb && !offCb.checked) {
err.textContent = 'Tick "I checked: the box is off" first.';
return;
}
if (offCb) { document.getElementById('host-delete-off-hidden-' + hostID).value = '1'; }
document.getElementById('host-delete-confirm-hidden-' + hostID).value = typed;
document.getElementById('host-delete-escrow-hidden-' + hostID).value = escrowCb.checked ? '1' : '';
document.getElementById('host-delete-form-' + hostID).submit();
}
</script>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hosts — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<script>
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-sm btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-sm btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
</script>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<h2 style="margin-bottom: 1rem;">Hosts</h2>
<section class="card" id="held-apps" style="margin-bottom: 1.5rem; padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem; margin-top: 0;">Held apps</h2>
<table class="data-table">
<thead>
<tr>
<th>Customer</th>
<th>App</th>
<th>Reason</th>
<th>Since</th>
<th>Who may clear it</th>
</tr>
</thead>
<tbody>
<tr data-held-app="demo-hp/gokapi">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>gokapi</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaosoom">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaosoom</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaoscrash">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaoscrash</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
<tr data-held-app="demo-hp/chaosoomb">
<td><a href="/hosts/d53-test-9202#held-apps">demo-hp</a></td>
<td><code>chaosoomb</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>the household&#39;s Start (one more try)</td>
</tr>
</tbody>
</table>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<table class="data-table">
<thead>
<tr>
<th>Host</th>
<th>Customer</th>
<th>Agent</th>
<th>Proxmox / kernel</th>
<th>Status</th>
<th>Guests</th>
<th>CPU</th>
<th>Mem</th>
<th>Disk</th>
<th>Cloudflared</th>
<th>Worst Storage</th>
</tr>
</thead>
<tbody>
<tr onclick="window.location='/hosts/d53-test-9202'" style="cursor: pointer;">
<td><a href="/hosts/d53-test-9202">d53-test-9202</a></td>
<td>Demo HP (scratch guest 9202)</td>
<td>—</td>
<td style="font-size: 0.85em;">unknown<br><span class="text-muted">unknown</span></td>
<td><span class="status-badge status-badge-pending">NO REPORT</span></td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>—</td>
</tr>
</tbody>
</table>
</section>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>d53-test-9202 — Felhom Hub</title>
<link rel="stylesheet" href="/style.css?v=0.173.0-d53test">
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link active">Hosts</a>
<a href="/system" class="nav-link">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<a href="/hosts" class="back-link">&larr; Hosts</a>
<section class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h2 style="margin: 0;">d53-test-9202</h2>
<span class="status-badge status-badge-pending">NO REPORT</span>
</div>
<div class="info-grid" style="margin-top: 1rem;">
<div class="info-item">
<span class="label">Host ID</span>
<span class="value" style="font-family: var(--font-mono)">d53-test-9202</span>
</div>
<div class="info-item">
<span class="label">Customer</span>
<span class="value"><a href="/customers/demo-hp">Demo HP (scratch guest 9202)</a></span>
</div>
<div class="info-item">
<span class="label">Agent Version</span>
<span class="value">—</span>
</div>
<div class="info-item">
<span class="label">Enrolled</span>
<span class="value">22 min ago</span>
</div>
<div class="info-item">
<span class="label">Last Report</span>
<span class="value">waiting for first report</span>
</div>
<div class="info-item">
<span class="label">Box connection</span>
<span class="value" data-box-connection="not_connected" style="color: var(--warn)">not connected since the hub started (2026-10-26 17:28 UTC, 23 min ago)</span>
</div>
<div class="info-item">
<span class="label">Desired Generation</span>
<span class="value">0</span>
</div>
</div>
</section>
<section class="card">
<div class="empty-state">
<p>Waiting for first report.</p>
<p class="hint">Vitals, guests and storage appear once this host's agent sends a host-report.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Guests</h2>
<div class="empty-state" style="border: none;">
<p>No guests reported on this host.</p>
</div>
</section>
<section class="card" id="held-apps" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Held apps</h2>
<table class="data-table">
<thead>
<tr>
<th>App</th>
<th>Reason</th>
<th>Since</th>
<th>Why</th>
<th>Who may clear it</th>
</tr>
</thead>
<tbody>
<tr data-held-app="gokapi">
<td><code>gokapi</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a crash loop (2026-09-24T10:37:48Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaosoom">
<td><code>chaosoom</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a oom storm (2026-09-24T11:48:01Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaoscrash">
<td><code>chaoscrash</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a crash loop (2026-09-24T12:16:24Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
<tr data-held-app="chaosoomb">
<td><code>chaosoomb</code></td>
<td><span class="severity-badge severity-warning">unhealthy_stop</span></td>
<td>32d ago</td>
<td>stopped by the box for a oom storm (2026-09-24T12:39:51Z) — the household&#39;s Start gives it one more try</td>
<td>the household&#39;s Start (one more try) <span class="text-muted">(operator reminded)</span></td>
</tr>
</tbody>
</table>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Storage Targets</h2>
<div class="empty-state" style="border: none;">
<p>No storage targets reported.</p>
</div>
</section>
<section class="card" style="padding: 0; overflow: hidden;">
<h2 style="padding: 1.25rem 1.25rem 0.5rem;">Capabilities</h2>
<div class="empty-state" style="border: none;">
<p>No capability report yet (waiting for the first host report).</p>
</div>
</section>
<section class="card">
<h2>Diagnostics — Log Bundles</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
Pull-based: the box ships its debug ring on its own next cycle — controller &le; one report interval (~15 min),
agent &asymp; one heartbeat. The pull is recorded in the box's own log (customer-visible). Bundles expire after 72 h.
</p>
<div style="display: flex; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="controller">
<button type="submit" class="btn btn-sm">Request controller logs</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/request-logs" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="component" value="agent">
<button type="submit" class="btn btn-sm">Request agent logs</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No log bundles. Use the request buttons above — the box delivers on its next cycle.</p>
</div>
</section>
<section class="card" id="operator-actions">
<h2>Operator Actions</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
The box acts on its next report reply (usually seconds, at most one report interval) and answers on the
report after that. Each press runs once. Recorded in the box's own log and in Events. Unanswered after a day: expired.
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0;">
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="offsite_backup_now">
<button type="submit" class="btn btn-sm">Run off-site backup now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="run_job">
<select name="arg"><option value="disk-health-check">disk-health-check</option><option value="fill-watch">fill-watch</option><option value="offsite-integrity">offsite-integrity</option><option value="offsite-proof">offsite-proof</option></select>
<button type="submit" class="btn btn-sm">Run check now</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_stop">
<button type="submit" class="btn btn-sm btn-outline">Stop deletion countdown</button>
</form>
<form method="POST" action="/hosts/d53-test-9202/operator-action" style="display: inline;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="action" value="abandon_extend">
<input type="number" name="arg" min="1" max="30" value="7" style="width: 4.5em; padding: 0.3em 0.5em;" aria-label="days">
<button type="submit" class="btn btn-sm btn-outline">Extend countdown (days from now)</button>
</form>
</div>
<div class="empty-state" style="border: none;">
<p>No operator actions for this box yet.</p>
</div>
</section>
<section class="card">
<h2>Network</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">WireGuard</span>
<span class="value">
<span class="text-muted">no peer allocated</span>
</span>
</div>
</div>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 0.75rem;">
Waiting for the first host report.
</p>
</section>
<section class="card">
<h2>Guest network
<span class="badge badge-neutral" title="A capable agent sent no guest-network stanza">unknown</span>
</h2>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
This host reported no guest-network state, so it is <strong>unknown</strong> &mdash; not healthy.
The watchdog is default-on; a silent capable agent means it was switched off
(<code>guest_net.disable</code>), or this report predates it on this box.
</p>
</section>
<section class="card">
<h2>DR / Backup</h2>
<div class="info-grid">
<div class="info-item">
<span class="label">DR Recipe</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item">
<span class="label">Key Escrow</span>
<span class="value"><span class="text-muted">none</span></span>
</div>
<div class="info-item" id="offsite-integrity">
<span class="label">Last full check (off-site)</span>
<span class="value">not reported (no off-site status in the box&#39;s report)</span>
</div>
</div>
</section>
<section class="card">
<h2>Console access</h2>
<p><span class="badge badge-neutral">not vaulted</span></p>
<p class="hint" style="color: var(--text-muted); font-size: 0.85rem;">
No console credential is vaulted for this host. Expected for a byo host — the owner manages root@pam. Otherwise the installer's step 4b did not run; re-run felhom-host-install.sh, or set and vault one per the break-glass runbook &sect;5.
</p>
</section>
<section class="card" style="border-color: var(--crit);">
<h2>Danger zone</h2>
<p class="text-muted" style="font-size: 0.85rem;">
Removing this host deletes its reports, guests, log bundles and WireGuard peer, and
permanently invalidates its API key — a still-running agent would receive 401s.
Re-enrollment requires the Day-0 passphrase flow.
</p>
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteConfirm('d53-test-9202')">Remove host&hellip;</button>
<div id="host-delete-confirm-d53-test-9202" style="display: none; margin-top: 0.75rem; padding: 0.75rem; border: 1px solid var(--crit); background: var(--crit-dim); border-radius: var(--radius); max-width: 44em;">
<p id="host-delete-impact-d53-test-9202" style="margin: 0 0 0.5rem; font-size: 0.9em;">&hellip;</p>
<label id="host-delete-escrow-row-d53-test-9202" style="display: none; margin: 0 0 0.5rem; font-size: 0.85em;">
<input type="checkbox" id="host-delete-escrow-d53-test-9202">
Move key escrow to retained custody (required when escrow present) + remove DR bundle for this host
</label>
<p style="margin: 0 0 0.5rem; font-size: 0.85em; color: var(--text-2);">Type the host id to confirm:</p>
<form method="POST" action="/hosts/d53-test-9202/delete" id="host-delete-form-d53-test-9202" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input type="hidden" name="_csrf" value="<redacted-test-csrf>">
<input type="hidden" name="confirm_host_id" id="host-delete-confirm-hidden-d53-test-9202" value="">
<input type="hidden" name="delete_escrow" id="host-delete-escrow-hidden-d53-test-9202" value="">
<input type="text" id="host-delete-confirm-input-d53-test-9202" placeholder="retype the host id&hellip;" style="padding: 0.3em 0.5em; width: 16em;">
<button type="button" class="btn btn-danger btn-sm" onclick="hostDeleteSubmit('d53-test-9202')">Confirm &amp; remove</button>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('host-delete-confirm-d53-test-9202').style.display='none';">Cancel</button>
</form>
<p id="host-delete-err-d53-test-9202" style="margin: 0.4em 0 0; font-size: 0.8em; color: var(--crit);"></p>
</div>
</section>
<script>
function hostDeleteConfirm(hostID) {
var box = document.getElementById('host-delete-confirm-' + hostID);
var impact = document.getElementById('host-delete-impact-' + hostID);
document.getElementById('host-delete-confirm-input-' + hostID).value = '';
document.getElementById('host-delete-err-' + hostID).textContent = '';
box.style.display = 'block';
impact.textContent = 'Checking impact…';
fetch('/hosts/' + encodeURIComponent(hostID) + '/delete-impact')
.then(function(r){ return r.json(); })
.then(function(d){
var parts = ['Deleting ' + hostID + ' removes ' + d.guests + ' guest row(s), ' +
d.reports + ' host report(s), ' + d.log_bundles + ' agent log bundle(s)'];
if (d.wg_peer_bound) parts.push('the bound WireGuard peer');
if (d.pbs_secret_present) parts.push('the staged PBS secret');
if (d.recovery_present) parts.push('the break-glass recovery credential');
impact.textContent = parts.join(', ') + '. Host status: ' + d.status + '.' +
(d.deletable ? (d.off_tick_required ? ' No box connection for 6+ minutes — the "box is off" tick is required.' : '') : ' Host is ONLINE — deletion will be refused.');
document.getElementById('host-delete-escrow-row-' + hostID).style.display =
d.escrow_present ? 'block' : 'none';
})
.catch(function(){ impact.textContent = 'Could not compute the impact — the server will still enforce every gate.'; });
}
function hostDeleteSubmit(hostID) {
var typed = document.getElementById('host-delete-confirm-input-' + hostID).value.trim();
var err = document.getElementById('host-delete-err-' + hostID);
if (typed !== hostID) { err.textContent = 'Confirmation does not match the host id.'; return; }
var escrowRow = document.getElementById('host-delete-escrow-row-' + hostID);
var escrowCb = document.getElementById('host-delete-escrow-' + hostID);
if (escrowRow.style.display !== 'none' && !escrowCb.checked) {
err.textContent = 'This host has a key escrow — tick the acknowledgement to delete it too.';
return;
}
var offCb = document.getElementById('host-delete-off-' + hostID);
if (offCb && !offCb.checked) {
err.textContent = 'Tick "I checked: the box is off" first.';
return;
}
if (offCb) { document.getElementById('host-delete-off-hidden-' + hostID).value = '1'; }
document.getElementById('host-delete-confirm-hidden-' + hostID).value = typed;
document.getElementById('host-delete-escrow-hidden-' + hostID).value = escrowCb.checked ? '1' : '';
document.getElementById('host-delete-form-' + hostID).submit();
}
</script>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">0.173.0-d53test</span>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,253 @@
2026/10/26 17:29:52 notifier.go:81: [INFO] Notifier enabled (hub: http://192.168.0.180:18090)
2026/10/26 17:29:52 scheduler.go:120: [INFO] [scheduler] Registered periodic job: hold-report (every 1m0s)
2026/10/26 17:29:52 scheduler.go:85: [DEBUG] [scheduler] periodic job registered: name="hold-report" interval=1m0s totalJobs=8
2026/10/26 17:29:52 notifier.go:1145: [DEBUG] SyncPreferences: url=http://192.168.0.180:18090/api/v1/preferences source=default email= events=[backup_failed db_dump_failed disk_warning disk_critical storage_disconnected node_down health_critical expected_backup_missed expected_dbdump_missed offbox_enlarge_blocked app_update_undone app_update_held app_stopped_unhealthy storage_reconnected backup_integrity_failed crossdrive_failed health_recovered] cooldown=6h
2026/10/26 17:29:52 notifier.go:1176: [INFO] Notification preferences synced to hub: source=default status=ok email=, events=[backup_failed db_dump_failed disk_warning disk_critical storage_disconnected node_down health_critical expected_backup_missed expected_dbdump_missed offbox_enlarge_blocked app_update_undone app_update_held app_stopped_unhealthy storage_reconnected backup_integrity_failed crossdrive_failed health_recovered], cooldown=6h, email_cleared=false
2026/10/26 17:29:52 sync.go:559: [DEBUG] [sync] radicale/docker-compose.yml: hash match, skipped
2026/10/26 17:29:52 sync.go:559: [DEBUG] [sync] radicale/.felhom.yml: hash match, skipped
2026/10/26 17:30:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:30:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:30:52 holdreport.go:137: [INFO] [holdreport] chaoscrash held (unhealthy_stop since 2026-09-24T12:16:24Z) — reported to the hub
2026/10/26 17:30:52 holdreport.go:137: [INFO] [holdreport] chaosoom held (unhealthy_stop since 2026-09-24T11:48:01Z) — reported to the hub
2026/10/26 17:30:52 holdreport.go:137: [INFO] [holdreport] chaosoomb held (unhealthy_stop since 2026-09-24T12:39:51Z) — reported to the hub
2026/10/26 17:30:52 holdreport.go:137: [INFO] [holdreport] gokapi held (unhealthy_stop since 2026-09-24T10:37:48Z) — reported to the hub
2026/10/26 17:30:52 holdreport.go:137: [DEBUG] [holdreport] full re-send: 4 hold(s) sent, 0 cleared
2026/10/26 17:30:52 notifier.go:317: [DEBUG] PushEvent: type=app_held severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:30:52 notifier.go:317: [DEBUG] PushEvent: type=app_held severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:30:52 notifier.go:317: [DEBUG] PushEvent: type=app_held severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:30:52 notifier.go:317: [DEBUG] PushEvent: type=app_held severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:30:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 1ms)
2026/10/26 17:30:52 notifier.go:343: [DEBUG] PushEvent: app_held pushed OK (HTTP 200)
2026/10/26 17:30:52 notifier.go:355: [INFO] Event pushed: app_held (info) [hu-only] — App chaosoom is held: stopped by the box for a oom storm (2026-09-24T11:48:01Z) — the household's Start gives it one more try
2026/10/26 17:30:52 notifier.go:343: [DEBUG] PushEvent: app_held pushed OK (HTTP 200)
2026/10/26 17:30:52 notifier.go:355: [INFO] Event pushed: app_held (info) [hu-only] — App gokapi is held: stopped by the box for a crash loop (2026-09-24T10:37:48Z) — the household's Start gives it one more try
2026/10/26 17:30:52 notifier.go:343: [DEBUG] PushEvent: app_held pushed OK (HTTP 200)
2026/10/26 17:30:52 notifier.go:355: [INFO] Event pushed: app_held (info) [hu-only] — App chaoscrash is held: stopped by the box for a crash loop (2026-09-24T12:16:24Z) — the household's Start gives it one more try
2026/10/26 17:30:52 notifier.go:343: [DEBUG] PushEvent: app_held pushed OK (HTTP 200)
2026/10/26 17:30:52 notifier.go:355: [INFO] Event pushed: app_held (info) [hu-only] — App chaosoomb is held: stopped by the box for a oom storm (2026-09-24T12:39:51Z) — the household's Start gives it one more try
2026/10/26 17:31:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:31:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:31:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:32:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:32:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:32:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:33:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:33:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:33:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:34:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:34:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:34:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:35:41 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:35:41 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:35:41 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale/deploy-fields
2026/10/26 17:35:41 router.go:78: [DEBUG] [api] GET /api/stacks/radicale/deploy-fields (path=/stacks/radicale/deploy-fields)
2026/10/26 17:35:41 auth.go:144: [DEBUG] [web] auth: valid session for POST /api/stacks/radicale/deploy
2026/10/26 17:35:41 router.go:78: [DEBUG] [api] POST /api/stacks/radicale/deploy (path=/stacks/radicale/deploy)
2026/10/26 17:35:41 router.go:442: [INFO] [api] Deploy requested for stack: radicale
2026/10/26 17:35:41 router.go:78: [DEBUG] [api] deployStack: name=radicale contentLength=145
2026/10/26 17:35:41 deploy.go:338: [DEBUG] Deploy radicale: received 4 user values
2026/10/26 17:35:41 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/radicale — 4 env vars, 1 encrypted, 1 sensitive fields
2026/10/26 17:35:41 [INFO] [stacks] SaveAppConfig: saved config for radicale
2026/10/26 17:35:41 deploy.go:574: [INFO] [stacks] Deploying stack radicale with 4 env vars: [SUBDOMAIN, RADICALE_USER, RADICALE_PASSWORD, DOMAIN]
2026/10/26 17:35:41 manager.go:1856: [INFO] [stacks] Deploying stack radicale — checking 1 images...
2026/10/26 17:35:41 manager.go:1860: [DEBUG] ghcr.io/kozea/radicale:3.8.1@sha256:54d9406cd30f9e206a9dd6d61dfac48da26b9afc37200e14f23beb8fafa7d11c — not found locally, will pull
2026/10/26 17:35:41 lifecycle.go:70: [DEBUG] [integrations] OnStackStart: stack=radicale, waiting 5s for state refresh
2026/10/26 17:35:41 manager.go:1701: [DEBUG] Running: docker compose up -d (in /opt/docker/stacks/radicale)
2026/10/26 17:35:46 lifecycle.go:86: [DEBUG] [integrations] OnStackStart: stack=radicale integrationsFound=0
2026/10/26 17:35:46 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:35:46 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:35:50 deploy.go:664: [INFO] [stacks] Stack radicale deployed successfully (took 9.0s)
2026/10/26 17:35:50 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/radicale — 4 env vars, 1 encrypted, 1 sensitive fields
2026/10/26 17:35:50 [INFO] [stacks] SaveAppConfig: saved config for radicale
2026/10/26 17:35:50 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/radicale — 4 env vars, 0 encrypted, 1 sensitive fields
2026/10/26 17:35:50 [INFO] [stacks] SaveAppConfig: saved config for radicale
2026/10/26 17:35:50 installed.go:420: [INFO] [stacks] installed-images radicale: recorded 1 service(s) (radicale=ghcr.io/kozea/radicale:3.8.1 (sha256:54d9406cd30f…))
2026/10/26 17:35:50 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/radicale — 4 env vars, 0 encrypted, 1 sensitive fields
2026/10/26 17:35:50 [INFO] [stacks] SaveAppConfig: saved config for radicale
2026/10/26 17:35:50 pin.go:93: [INFO] [stacks] pin radicale: radicale=ghcr.io/kozea/radicale:3.8.1
2026/10/26 17:35:50 [DEBUG] [backup] radicale: off-site default not applied: no off-site target
2026/10/26 17:35:51 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:35:51 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:35:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:35:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:35:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:35:53 manager.go:1701: [DEBUG] Running: docker compose ps -a --format table {{.Name}} {{.Image}} {{.State}} {{.Status}} (in /opt/docker/stacks/radicale)
2026/10/26 17:35:53 manager.go:1817: [INFO] [stacks] Stack radicale post-start status:
2026/10/26 17:35:53 manager.go:1820: [INFO] [stacks] radicale ghcr.io/kozea/radicale:3.8.1@sha256:54d9406cd30f9e206a9dd6d61dfac48da26b9afc37200e14f23beb8fafa7d11c running Up 3 seconds (health: starting)
2026/10/26 17:35:56 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:35:56 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:36:01 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:36:01 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:36:06 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:36:06 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:36:12 healthprobe.go:187: [DEBUG] Health probe radicale: API GET :5232/.web/ → 200 (4ms)
2026/10/26 17:36:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:36:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:36:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:36:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:36:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:36:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:37:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:37:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:37:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:37:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:37:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:37:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:37:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:37:52 main.go:4311: [WARN] [deadapp] radicale: crash_loop — 7 in 10m0s; STOPPING it (decision 28)
2026/10/26 17:37:52 manager.go:1443: [DEBUG] [stacks] StopStack radicale: current state=starting deployed=true containers=1
2026/10/26 17:37:52 manager.go:1446: [INFO] [stacks] Stopping stack: radicale
2026/10/26 17:37:52 manager.go:1701: [DEBUG] Running: docker compose down (in /opt/docker/stacks/radicale)
2026/10/26 17:37:52 manager.go:1455: [INFO] [stacks] Stack radicale stopped successfully (took 0.3s)
2026/10/26 17:37:52 settings.go:2043: [WARN] [settings] restore hold SET for radicale — the app stays stopped until it is cleared
2026/10/26 17:37:52 update_guard.go:873: [WARN] [backup] radicale is STOPPED by the box: crash_loop (trip 1 within 24h0m0s) — Start gives it one more try (decision 28)
2026/10/26 17:37:52 notifier.go:317: [DEBUG] PushEvent: type=app_stopped_unhealthy severity=warning url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:37:52 notifier.go:343: [DEBUG] PushEvent: app_stopped_unhealthy pushed OK (HTTP 200)
2026/10/26 17:37:52 notifier.go:355: [INFO] Event pushed: app_stopped_unhealthy (warning) [hu-only] — A(z) radicale alkalmazást leállítottuk, mert újra és újra összeomlott (vagy elfogyott a memóriája). Az Indítás gombbal újra megpróbálhatod.
2026/10/26 17:38:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:38:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:38:52 holdreport.go:137: [INFO] [holdreport] radicale held (unhealthy_stop since 2026-10-26T17:37:52Z) — reported to the hub
2026/10/26 17:38:52 notifier.go:317: [DEBUG] PushEvent: type=app_held severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:38:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 1ms)
2026/10/26 17:38:52 notifier.go:343: [DEBUG] PushEvent: app_held pushed OK (HTTP 200)
2026/10/26 17:38:52 notifier.go:355: [INFO] Event pushed: app_held (info) [hu-only] — App radicale is held: stopped by the box for a crash loop (2026-10-26T17:37:52Z) — the household's Start gives it one more try
2026/10/26 17:39:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:39:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:39:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:39:52 backup.go:540: [DEBUG] groupStacksByDrive: /mnt/sys_drive → [privatebin, radicale]
2026/10/26 17:40:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:40:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:40:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:41:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:41:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:41:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:42:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:42:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:42:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:43:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:43:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:43:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:44:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:44:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:44:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:44:52 backup.go:540: [DEBUG] groupStacksByDrive: /mnt/sys_drive → [privatebin, radicale]
2026/10/26 17:44:52 sync.go:559: [DEBUG] [sync] radicale/docker-compose.yml: hash match, skipped
2026/10/26 17:44:52 sync.go:559: [DEBUG] [sync] radicale/.felhom.yml: hash match, skipped
2026/10/26 17:45:37 auth.go:144: [DEBUG] [web] auth: valid session for POST /api/stacks/radicale/start
2026/10/26 17:45:37 router.go:78: [DEBUG] [api] POST /api/stacks/radicale/start (path=/stacks/radicale/start)
2026/10/26 17:45:37 router.go:663: [INFO] [api] start requested for stack: radicale
2026/10/26 17:45:37 router.go:78: [DEBUG] [api] actionStack: action=start name=radicale
2026/10/26 17:45:37 settings.go:2960: [INFO] [settings] unhealthy-stop hold LIFTED for radicale (Start)
2026/10/26 17:45:37 router.go:687: [INFO] [api] radicale: the unhealthy stop is LIFTED by Start — one more try (decision 28)
2026/10/26 17:45:37 manager.go:1354: [DEBUG] [stacks] StartStack radicale: current state=stopped deployed=true
2026/10/26 17:45:37 manager.go:1357: [INFO] [stacks] Starting stack: radicale
2026/10/26 17:45:37 manager.go:1364: [DEBUG] [stacks] StartStack radicale: prepared 10 env vars for compose
2026/10/26 17:45:37 manager.go:1701: [DEBUG] Running: docker compose up -d (in /opt/docker/stacks/radicale)
2026/10/26 17:45:37 manager.go:1372: [INFO] [stacks] Stack radicale started successfully (took 0.4s)
2026/10/26 17:45:38 installed.go:409: [DEBUG] [stacks] installed-images radicale: unchanged (1 service(s)) — app.yaml not rewritten
2026/10/26 17:45:38 lifecycle.go:70: [DEBUG] [integrations] OnStackStart: stack=radicale, waiting 5s for state refresh
2026/10/26 17:45:41 manager.go:1701: [DEBUG] Running: docker compose ps -a --format table {{.Name}} {{.Image}} {{.State}} {{.Status}} (in /opt/docker/stacks/radicale)
2026/10/26 17:45:41 manager.go:1817: [INFO] [stacks] Stack radicale post-start status:
2026/10/26 17:45:41 manager.go:1820: [INFO] [stacks] radicale ghcr.io/kozea/radicale:3.8.1@sha256:54d9406cd30f9e206a9dd6d61dfac48da26b9afc37200e14f23beb8fafa7d11c running Up 3 seconds (health: starting)
2026/10/26 17:45:43 lifecycle.go:86: [DEBUG] [integrations] OnStackStart: stack=radicale integrationsFound=0
2026/10/26 17:45:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:45:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:45:52 holdreport.go:137: [INFO] [holdreport] radicale hold cleared — reported to the hub
2026/10/26 17:45:52 notifier.go:317: [DEBUG] PushEvent: type=app_hold_cleared severity=info url=http://192.168.0.180:18090/api/v1/event
2026/10/26 17:45:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 1ms)
2026/10/26 17:45:52 notifier.go:343: [DEBUG] PushEvent: app_hold_cleared pushed OK (HTTP 200)
2026/10/26 17:45:52 notifier.go:355: [INFO] Event pushed: app_hold_cleared (info) [hu-only] — App radicale is no longer held
2026/10/26 17:45:52 healthprobe.go:187: [DEBUG] Health probe radicale: API GET :5232/.web/ → 200 (3ms)
2026/10/26 17:46:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:46:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:46:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:46:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:46:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:46:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:46:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:46:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:46:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 1m50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:47:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:47:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:47:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:47:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 2m50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:48:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:48:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:48:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:48:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 3m50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:49:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:49:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:49:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:49:52 backup.go:540: [DEBUG] groupStacksByDrive: /mnt/sys_drive → [privatebin, radicale]
2026/10/26 17:49:52 dbdump.go:149: [DEBUG] DiscoverDatabases: docker ps output: 9fa4e86f3613 radicale radicale ghcr.io/kozea/radicale:3.8.1
2026/10/26 17:49:52 dbdump.go:171: [DEBUG] DiscoverDatabases: skipping container radicale (image=ghcr.io/kozea/radicale:3.8.1, not a database)
2026/10/26 17:49:52 recovery_unit.go:297: [INFO] [backup] Recovery unit captured for radicale → /mnt/sys_drive/felhom-data/backups/primary/radicale (images=1, secrets-referenced=1, data_keys=0, portable-carried=0/0, withheld=1)
2026/10/26 17:50:02 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:50:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:50:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:50:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m40s ago, effective interval 5m0s, healthy=true
2026/10/26 17:50:42 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 4m50s ago, effective interval 5m0s, healthy=true
2026/10/26 17:50:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:50:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:50:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:50:52 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 5m0s ago, effective interval 5m0s, healthy=true
2026/10/26 17:51:02 healthprobe.go:187: [DEBUG] Health probe radicale: API GET :5232/.web/ → 200 (3ms)
2026/10/26 17:51:12 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 10s ago, effective interval 5m0s, healthy=true
2026/10/26 17:51:22 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 20s ago, effective interval 5m0s, healthy=true
2026/10/26 17:51:32 healthprobe.go:64: [DEBUG] [stacks] RunHealthProbes: skipping radicale — last check 30s ago, effective interval 5m0s, healthy=true
2026/10/26 17:51:34 auth.go:144: [DEBUG] [web] auth: valid session for POST /api/stacks/radicale/stop
2026/10/26 17:51:34 router.go:78: [DEBUG] [api] POST /api/stacks/radicale/stop (path=/stacks/radicale/stop)
2026/10/26 17:51:34 router.go:663: [INFO] [api] stop requested for stack: radicale
2026/10/26 17:51:34 router.go:78: [DEBUG] [api] actionStack: action=stop name=radicale
2026/10/26 17:51:34 [DEBUG] [stacks] SaveAppConfig: saving /opt/docker/stacks/radicale — 4 env vars, 0 encrypted, 1 sensitive fields
2026/10/26 17:51:34 [INFO] [stacks] SaveAppConfig: saved config for radicale
2026/10/26 17:51:34 desiredstate.go:81: [INFO] [stacks] desired state for radicale recorded as "stopped" (was "running")
2026/10/26 17:51:34 manager.go:1443: [DEBUG] [stacks] StopStack radicale: current state=running deployed=true containers=1
2026/10/26 17:51:34 manager.go:1446: [INFO] [stacks] Stopping stack: radicale
2026/10/26 17:51:34 manager.go:1701: [DEBUG] Running: docker compose down (in /opt/docker/stacks/radicale)
2026/10/26 17:51:35 manager.go:1455: [INFO] [stacks] Stack radicale stopped successfully (took 0.4s)
2026/10/26 17:51:35 lifecycle.go:24: [DEBUG] [integrations] OnStackStop: stack=radicale integrationsFound=0
2026/10/26 17:51:40 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:51:40 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
2026/10/26 17:51:40 auth.go:144: [DEBUG] [web] auth: valid session for POST /api/stacks/radicale/remove
2026/10/26 17:51:40 router.go:78: [DEBUG] [api] POST /api/stacks/radicale/remove (path=/stacks/radicale/remove)
2026/10/26 17:51:40 router.go:1025: [INFO] [api] Remove requested for stack: radicale
2026/10/26 17:51:40 router.go:78: [DEBUG] [api] removeStack: name=radicale
2026/10/26 17:51:40 router.go:78: [DEBUG] [api] removeStack: name=radicale removeHDDData=true removeBackups=true
2026/10/26 17:51:40 delete.go:626: [DEBUG] [stacks] RemoveStack called: name="radicale", removeHDDData=true, backupPathsToRemove=3
2026/10/26 17:51:40 delete.go:640: [DEBUG] [stacks] RemoveStack radicale: state=stopped, deployed=true, orphaned=false, deploying=false
2026/10/26 17:51:40 delete.go:697: [INFO] Removing deployed stack: radicale (removeHDDData=true, hddDeclared=false, backupPaths=3)
2026/10/26 17:51:40 delete.go:710: [DEBUG] [stacks] RemoveStack radicale: found 0 HDD mounts from compose file
2026/10/26 17:51:40 manager.go:1701: [DEBUG] Running: docker compose down --volumes (in /opt/docker/stacks/radicale)
2026/10/26 17:51:40 delete.go:736: [DEBUG] [stacks] RemoveStack radicale: compose down output:
2026/10/26 17:51:52 scheduler.go:450: [INFO] [scheduler] Running job: hold-report
2026/10/26 17:51:52 scheduler.go:85: [DEBUG] [scheduler] job hold-report: execution starting
2026/10/26 17:51:52 scheduler.go:467: [INFO] [scheduler] Job hold-report completed (took 0s)
2026/10/26 17:52:06 delete.go:774: [INFO] [stacks] RemoveStack radicale: removed volume(s) [radicale_radicale_data]
2026/10/26 17:52:06 delete.go:837: [DEBUG] [stacks] RemoveStack radicale: processing 3 backup paths for removal (base=/mnt/sys_drive/felhom-data/backups)
2026/10/26 17:52:06 delete.go:854: [INFO] Removed backup data: /mnt/sys_drive/felhom-data/backups/primary/radicale (36K)
2026/10/26 17:52:06 delete.go:862: [DEBUG] [stacks] RemoveStack radicale: removing app.yaml at /opt/docker/stacks/radicale/app.yaml
2026/10/26 17:52:06 delete.go:879: [INFO] Stack radicale removed successfully (took 26.8s)
2026/10/26 17:52:07 lifecycle.go:176: [DEBUG] [integrations] OnStackRemove: stack=radicale integrationsFound=0
2026/10/26 17:52:07 notifier.go:355: [INFO] Event pushed: app_removed (info) [hu-only] — Alkalmazás eltávolítva: radicale
2026/10/26 17:52:07 image_retention.go:265: [INFO] [stacks] image retention (remove of radicale): deleted [ghcr.io/kozea/radicale:<none>] (0139a3fcd1ed, 96.6MB) — no container, installed app or undo names it (decision 53)
2026/10/26 17:52:07 image_retention.go:238: [INFO] [stacks] image retention (remove of radicale): pass over 40 image(s) of [ghcr.io/kozea/radicale] — 1 candidate(s), 1 deleted, the rest kept
2026/10/26 17:52:12 auth.go:144: [DEBUG] [web] auth: valid session for GET /api/stacks/radicale
2026/10/26 17:52:12 router.go:78: [DEBUG] [api] GET /api/stacks/radicale (path=/stacks/radicale)
privatebin
@@ -0,0 +1 @@
held_apps after: [('chaosoom', 'unhealthy_stop'), ('gokapi', 'unhealthy_stop'), ('chaoscrash', 'unhealthy_stop'), ('chaosoomb', 'unhealthy_stop')]
@@ -0,0 +1,14 @@
['id', 'customer_id', 'event_type', 'severity', 'message', 'status', 'error_message', 'created_at', 'channel']
['1', 'demo-hp', 'app_definition_refused', 'warning', 'immich: the box REFUSED a catalog definition — it asks for c', 'sent', '', '2026-10-26 17:29:52', 'operator']
['2', 'demo-hp', 'app_definition_refused', 'warning', 'immich: the box REFUSED a catalog definition — it asks for c', 'skipped', 'operator_only', '2026-10-26 17:29:52', 'customer']
['3', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app gokapi has been held for 32 days (', 'sent', '', '2026-10-26 17:33:35', 'operator']
['4', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app gokapi has been held for 32 days (', 'skipped', 'operator_only', '2026-10-26 17:33:35', 'customer']
['5', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaosoom has been held for 32 days', 'sent', '', '2026-10-26 17:33:35', 'operator']
['6', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaosoom has been held for 32 days', 'skipped', 'operator_only', '2026-10-26 17:33:35', 'customer']
['7', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaoscrash has been held for 32 da', 'sent', '', '2026-10-26 17:33:35', 'operator']
['8', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaoscrash has been held for 32 da', 'skipped', 'operator_only', '2026-10-26 17:33:35', 'customer']
['9', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaosoomb has been held for 32 day', 'sent', '', '2026-10-26 17:33:35', 'operator']
['10', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app chaosoomb has been held for 32 day', 'skipped', 'operator_only', '2026-10-26 17:33:35', 'customer']
['11', 'demo-hp', 'app_stopped_unhealthy', 'warning', 'A(z) radicale alkalmazást leállítottuk, mert újra és újra ös', 'sent', '', '2026-10-26 17:37:53', 'operator']
['12', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app radicale has been held for 4m0s (u', 'sent', '', '2026-10-26 17:41:35', 'operator']
['13', 'demo-hp', 'app_held_reminder', 'warning', 'Customer demo-hp: the app radicale has been held for 4m0s (u', 'skipped', 'operator_only', '2026-10-26 17:41:35', 'customer']
@@ -0,0 +1,82 @@
2026/10/26 18:28:34 [INFO] felhom-hub 0.173.0-d53test starting
2026/10/26 18:28:34 [INFO] [store] app-update event types added to 0 household(s)' notification prefs (one-time, add-only): []
2026/10/26 18:28:34 [INFO] [store] app_stopped_unhealthy added to 0 household(s)' notification prefs (one-time, add-only): []
2026/10/26 18:28:34 [ERROR] OFFSITE_SECRET_KEY unset — off-site passwords cannot be sealed; provisioning and the key registrar are DISABLED
2026/10/26 18:28:34 [INFO] Database opened at /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/testhub/data/hub.db
2026/10/26 18:28:34 [WARN] Cannot read seed dir /usr/share/felhom/assets-seed: open /usr/share/felhom/assets-seed: no such file or directory (no assets will be available until uploaded)
2026/10/26 18:28:34 [INFO] Asset manifest built: 0 files
2026/10/26 18:28:34 [INFO] App-email relay enabled (limit 30/min/customer, From domains [felhom.eu])
2026/10/26 18:28:34 [INFO] osupdates: approval rule = every ring-0 box healthy for 24h0m0s and 1 night run(s)
2026/10/26 18:28:34 [INFO] osupdates: the Docker engine set is approved only by the operator, after 2 healthy ring-0 night(s)
2026/10/26 18:28:34 [INFO] osupdates: alarms = no OS leg 168h0m0s, reboot needed 336h0m0s, ring 0 stalled 168h0m0s, not covered 336h0m0s
2026/10/26 18:28:34 [INFO] access log: /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/testhub/data/access-log, one file per UTC day, kept 90 days
2026/10/26 18:28:34 [INFO] Registry version checker disabled (no credentials configured)
2026/10/26 18:28:34 [INFO] WG peer-sync disabled (endpoint not configured)
2026/10/26 18:28:34 [INFO] PBS DR tenantsync disabled (key or endpoint not configured)
2026/10/26 18:28:34 [INFO] agent-plane poke disabled (key or endpoint not configured)
2026/10/26 18:28:34 [INFO] PBS-DR self-heal reconciler started (interval 5m)
2026/10/26 18:28:34 [INFO] offsite credential self-heal reconciler started (interval 5m, debounce 2 reports)
2026/10/26 18:28:34 [INFO] Staleness checker initialized: 0 ok, 0 stale, 0 down (node_stale after 45m0s, node_down after 1h30m0s)
2026/10/26 18:28:34 [INFO] prune: next run at 2026-10-27 04:30 CET (in 10h1m25s)
2026/10/26 18:28:34 [INFO] Host staleness checker initialized: 0 ok, 0 already alarmed (seeded silently, R-1001), 0 stale, 0 down (left unseeded → first Check emits; host_stale after 45m0s, host_down after 1h30m0s)
2026/10/26 18:28:34 [INFO] Host capability checker initialized: 0 ok, 0 degraded, 0 with a refused whole-guest backup (degraded/refused left unseeded → first Check emits)
2026/10/26 18:28:34 [INFO] Host leaf checker initialized: 0 host fingerprint(s) seeded
2026/10/26 18:28:34 [INFO] Host disk checker initialized: warn=90% crit=95%, 0 ok seeded, 0 already-breached left unseeded (first Check emits)
2026/10/26 18:28:34 [INFO] Storage fill checker initialized: warn=90% crit=95%, 0 ok seeded, 0 already-breached left unseeded, 0 root-backed excluded
2026/10/26 18:28:34 [INFO] Host mgmt-plane checker initialized: 0 host heal-state(s) seeded
2026/10/26 18:28:34 [INFO] Host OOB checker initialized: 0 host(s) seeded degraded
2026/10/26 18:28:34 [INFO] Offsite checker initialized: fill warn=90% crit=95%, stale after 48h0m0s, 0 ok-seeded
2026/10/26 18:28:34 [WARN] Held apps: the reminder grace is OVERRIDDEN to 3m0s for every reason — a test configuration
2026/10/26 18:28:34 [WARN] Golden vouch check OFF: no registry credentials, so the hub cannot list published goldens
2026/10/26 18:28:34 [INFO] db-snapshot 08:00: next run at 2026-10-27 08:00 CET (in 13h31m25s)
2026/10/26 18:28:34 [INFO] db-snapshot 20:00: next run at 2026-10-26 20:00 CET (in 1h31m25s)
2026/10/26 18:28:34 [INFO] deadline-check: next run at 2026-10-27 05:00 CET (in 10h31m25s)
2026/10/26 18:28:34 [INFO] Listening on 192.168.0.180:18090
2026/10/26 18:28:34 [INFO] db-snapshot 14:00: next run at 2026-10-27 14:00 CET (in 19h31m25s)
2026/10/26 18:28:34 [INFO] db-snapshot 02:00: next run at 2026-10-27 02:00 CET (in 7h31m25s)
2026/10/26 18:28:34 [INFO] db snapshot written: hub-20261026T172834Z.db (409600 bytes, 13ms); pruned 0, keeping 2
2026/10/26 18:29:34 [INFO] osupdates: guest candidate (0 packages, first seen 0001-01-01T00:00:00Z): waiting — no ring-0 box
2026/10/26 18:29:34 [INFO] osupdates: host candidate (0 packages, first seen 0001-01-01T00:00:00Z): waiting — no ring-0 box
2026/10/26 18:29:34 [INFO] osupdates: alarm check done in 4ms, 0 alarm(s) sent []
2026/10/26 18:29:52 [DEBUG] Notification prefs read for demo-hp: exists=false household_saved=false
2026/10/26 18:29:52 [INFO] Notification preferences updated for demo-hp: address set=false, household=false, events=[backup_failed db_dump_failed disk_warning disk_critical storage_disconnected node_down health_critical expected_backup_missed expected_dbdump_missed offbox_enlarge_blocked app_update_undone app_update_held app_stopped_unhealthy storage_reconnected backup_integrity_failed crossdrive_failed health_recovered]
2026/10/26 18:29:52 [INFO] Event from demo-hp: app_definition_refused (warning) — immich: the box REFUSED a catalog definition — it asks for container rights not on the box's allow-list. Nothing was written; the app keeps its last good compose. Check who pushed to the catalog. the catalog definition of immich asks for container rights the box does not allow: immich-server: bind /mnt/sizing-bench/appdata/immich -> /usr/src/app/upload rw; immich-server: bind /mnt/sizing-bench/userdata/media/photos -> /external/photos ro
2026/10/26 18:29:52 [INFO] Operator email sent for demo-hp/app_definition_refused
2026/10/26 18:29:57 [INFO] Event from demo-hp: controller_started (info) — Controller elindult (0.346.0-d53test)
2026/10/26 18:29:58 [INFO] DR-recipe app-half stored for customer demo-hp (v1)
2026/10/26 18:29:58 [INFO] Received report from demo-hp (8171 bytes)
2026/10/26 18:30:52 [INFO] Event from demo-hp: app_held (info) — App chaosoom is held: stopped by the box for a oom storm (2026-09-24T11:48:01Z) — the household's Start gives it one more try
2026/10/26 18:30:52 [DEBUG] [held-apps] demo-hp/chaosoom held (unhealthy_stop since 2026-09-24T11:48:01Z)
2026/10/26 18:30:52 [INFO] Event from demo-hp: app_held (info) — App gokapi is held: stopped by the box for a crash loop (2026-09-24T10:37:48Z) — the household's Start gives it one more try
2026/10/26 18:30:52 [DEBUG] [held-apps] demo-hp/gokapi held (unhealthy_stop since 2026-09-24T10:37:48Z)
2026/10/26 18:30:52 [INFO] Event from demo-hp: app_held (info) — App chaoscrash is held: stopped by the box for a crash loop (2026-09-24T12:16:24Z) — the household's Start gives it one more try
2026/10/26 18:30:52 [DEBUG] [held-apps] demo-hp/chaoscrash held (unhealthy_stop since 2026-09-24T12:16:24Z)
2026/10/26 18:30:52 [INFO] Event from demo-hp: app_held (info) — App chaosoomb is held: stopped by the box for a oom storm (2026-09-24T12:39:51Z) — the household's Start gives it one more try
2026/10/26 18:30:52 [DEBUG] [held-apps] demo-hp/chaosoomb held (unhealthy_stop since 2026-09-24T12:39:51Z)
2026/10/26 18:33:34 [INFO] Held apps: reminder for demo-hp/gokapi (unhealthy_stop, held 32 days, repeat=false)
2026/10/26 18:33:35 [INFO] Operator email sent for demo-hp/app_held_reminder
2026/10/26 18:33:35 [INFO] Held apps: reminder for demo-hp/chaosoom (unhealthy_stop, held 32 days, repeat=false)
2026/10/26 18:33:35 [INFO] Operator email sent for demo-hp/app_held_reminder
2026/10/26 18:33:35 [INFO] Held apps: reminder for demo-hp/chaoscrash (unhealthy_stop, held 32 days, repeat=false)
2026/10/26 18:33:35 [INFO] Operator email sent for demo-hp/app_held_reminder
2026/10/26 18:33:35 [INFO] Held apps: reminder for demo-hp/chaosoomb (unhealthy_stop, held 32 days, repeat=false)
2026/10/26 18:33:35 [INFO] Operator email sent for demo-hp/app_held_reminder
2026/10/26 18:35:41 [INFO] Event from demo-hp: app_deploy_started (info) — Alkalmazás telepítése elindult: Radicale
2026/10/26 18:35:44 [INFO] DR-recipe app-half stored for customer demo-hp (v1)
2026/10/26 18:35:44 [INFO] Received report from demo-hp (8070 bytes)
2026/10/26 18:35:50 [INFO] Event from demo-hp: app_deployed (info) — Alkalmazás telepítve: Radicale
2026/10/26 18:37:52 [INFO] Event from demo-hp: app_stopped_unhealthy (warning) — A(z) radicale alkalmazást leállítottuk, mert újra és újra összeomlott (vagy elfogyott a memóriája). Az Indítás gombbal újra megpróbálhatod.
2026/10/26 18:37:53 [INFO] Operator email sent for demo-hp/app_stopped_unhealthy
2026/10/26 18:38:52 [INFO] Event from demo-hp: app_held (info) — App radicale is held: stopped by the box for a crash loop (2026-10-26T17:37:52Z) — the household's Start gives it one more try
2026/10/26 18:38:52 [DEBUG] [held-apps] demo-hp/radicale held (unhealthy_stop since 2026-10-26T17:37:52Z)
2026/10/26 18:41:34 [INFO] Held apps: reminder for demo-hp/radicale (unhealthy_stop, held 4m0s, repeat=false)
2026/10/26 18:41:35 [INFO] Operator email sent for demo-hp/app_held_reminder
2026/10/26 18:44:52 [INFO] DR-recipe app-half stored for customer demo-hp (v1)
2026/10/26 18:44:52 [INFO] Received report from demo-hp (7771 bytes)
2026/10/26 18:45:52 [INFO] Event from demo-hp: app_hold_cleared (info) — App radicale is no longer held
2026/10/26 18:45:52 [INFO] [held-apps] demo-hp/radicale no longer held (a row existed: true)
2026/10/26 18:52:07 [INFO] Event from demo-hp: app_removed (info) — Alkalmazás eltávolítva: radicale
2026/10/26 18:52:09 [INFO] DR-recipe app-half stored for customer demo-hp (v1)
2026/10/26 18:52:09 [INFO] Received report from demo-hp (7122 bytes)
2026/10/26 18:52:49 [INFO] Received signal terminated, shutting down...
2026/10/26 18:52:49 [INFO] felhom-hub stopped
@@ -0,0 +1,55 @@
# R-1297 — faster CI: two runners, the trim, the numbers, the decoys (2026-10-26 evening)
## 1. Both runners work (R-1297 B, done by the operator)
Two harmless docs commits pushed to two repos, read from the Gitea jobs API (`runner_name` is the job page's runner):
| Job | Repo, commit | Runner | Started | Ended |
|---|---|---|---|---|
| 2374 | felhom.eu `8143a2c8` | `felhom-gates-runner` (id 2) | 17:11:15 | 17:17:06 |
| 2375 | felhom-agent `ad80ee7` | `felhom-gates-runner-2` (id 3) | 17:11:41 | 17:12:56 |
**At 17:11:41 UTC both jobs ran at the same time, on two different runners.** The agent job queued 3 s.
## 2. The trim (R-1297 A, `ecd82aba`)
- `scripts/script_tests_gate.py` `PATH_FILTERED`: on a CI **branch push with a known file list**, three suites run only
when one of their paths changed — `test_gate_decoys.py` (`scripts/`, `.gitea/`, `.githooks/`),
`test_due_checks_gate.py` (its gate, itself, `repo_gates.py`, `script_tests_gate.py`),
`dooplex-offsite/test_dooplex_offsite.py` (`scripts/dooplex-offsite/`). No list, an empty list or an unreadable list
runs every suite. The pre-push hook sets no list, so it still runs every suite.
- `.gitea/workflows/gates.yml`: a nightly schedule (03:20 UTC); the list is set only for `push:refs/heads/*`. A tag, the
nightly run and a manual dispatch run everything. A trimmed commits array (`total_commits` > listed) is an unknown list.
- `test_repo_gates.py` **tests the wiring** instead of running the whole gate set a second time: the real runner,
`GATES` narrowed by label to the five fingerprinted gates (130 s → 2.7 s). The same for `test_due_checks_gate.py`
group G (one more full run inside it, ~130 s → under 1 s).
- Red-proved: the filter cases and the workflow pins (`test_script_tests_gate.py`, red on the old `gates.yml`), an
inert `run_gate` (`test_repo_gates.py` red), an unregistered due-checks gate (group G red).
**Not done: a Go build cache for `hub-gofmt`.** The R-1297 row was wrong about it: CI has no Go toolchain, so `hub-gofmt`
says NOT CHECKED there in 0.1 s. The ~110 s after it in job 2366 was `security-scan` (gitleaks over the depth-1 tree);
on the 2-CPU runners it takes 27 s. Nothing to cache.
## 3. Times
| Commit | Before | After |
|---|---|---|
| (a) docs only | job 2370 (1 runner, 1 CPU): **810 s**; job 2374 (2 CPUs, before the trim): **351 s** | see the R-1297 row (the next docs-only push) |
| (b) touches a gate script | 351 s (every suite ran on every push) | job 2376 `ecd82aba`: **203 s** (decoys ran; off-site skipped) |
| (c) a hub change | 351 s | job 2378 `7e57e824`: **118 s** (all three skipped) |
Per-suite lines: `ci-job-*-suites.txt`.
## 4. Decoys — a broken gate still goes red with the filter ON
A red commit cannot be pushed (the pre-push hook refuses it; `--no-verify` is forbidden), so each decoy was run through
**CI's own entry point** (`repo_gates.py --fast`, `FELHOM_CHANGED_FILES` = the commit's file list, exactly as
`gates.yml` sets it) in a scratch clone of all four repos.
- **Decoy 1 (docs only):** R-1297's row loses its state and owner cells. All three heavy suites SKIPPED;
`register-shape` CONVICTED; **rc=1**. `decoy1.txt`
- **Decoy 2 (a gate made inert):** `register_shape_gate.py` main() convicts nothing. The decoy suite RAN (its path
changed) — and stayed **green, rc=0**: its register-shape cases called `check()` directly, never the script the runner
calls. A real hole, older than the trim. `decoy2.txt`
- **Fixed in the same session** (small; `test_gate_decoys.py` now asserts the script's exit code on every
register-shape case). **Decoy 2 again: 9 decoys exposed the hole, `script-tests` FAILED, rc=1.** `decoy2b.txt`
@@ -0,0 +1,40 @@
2026-10-26T13:38:54.0405384Z == gate: script-tests (script_tests_gate.py /root/.cache/act/cdd17f98aeae57bd/hostexecutor)
2026-10-26T13:49:17.0478186Z OK scripts/test_build_sizing_web.py 0.3s rc=0
2026-10-26T13:49:17.0753209Z OK scripts/test_dockerfile_arg_order.py 0.1s rc=0
2026-10-26T13:49:17.0753966Z OK scripts/test_due_checks_gate.py 135.3s rc=0
2026-10-26T13:49:17.0754283Z OK scripts/test_felhom_restore_beside.py 1.2s rc=0
2026-10-26T13:49:17.0754632Z OK scripts/test_gate_decoys.py 229.5s rc=0
2026-10-26T13:49:17.0754899Z OK scripts/test_golden_currency_gate.py 3.0s rc=0
2026-10-26T13:49:17.0755567Z OK scripts/test_guide_quote_gate.py 1.1s rc=0
2026-10-26T13:49:17.0755890Z OK scripts/test_host_swap_rule_gate.py 0.5s rc=0
2026-10-26T13:49:17.0756131Z OK scripts/test_hostinstall.py 1.7s rc=0
2026-10-26T13:49:17.0756359Z OK scripts/test_hu_grep.py 0.2s rc=0
2026-10-26T13:49:17.0756622Z OK scripts/test_hub_copy_drift.py 0.1s rc=0
2026-10-26T13:49:17.0756954Z OK scripts/test_instructions_gate.py 4.5s rc=0
2026-10-26T13:49:17.0757265Z OK scripts/test_iso_bootstrap_gate.py 2.2s rc=0
2026-10-26T13:49:17.0757563Z OK scripts/test_no_latest_push.py 0.1s rc=0
2026-10-26T13:49:17.0757833Z OK scripts/test_push_scope.py 0.1s rc=0
2026-10-26T13:49:17.0758121Z OK scripts/test_read_credential.py 0.3s rc=0
2026-10-26T13:49:17.0758362Z OK scripts/test_repo_gates.py 130.2s rc=0
2026-10-26T13:49:17.0758605Z OK scripts/test_repo_gates_docstring.py 0.1s rc=0
2026-10-26T13:49:17.0758848Z OK scripts/test_repo_gates_scope.py 0.8s rc=0
2026-10-26T13:49:17.0759179Z OK scripts/test_reuse_refs_check.py 0.5s rc=0
2026-10-26T13:49:17.0759423Z OK scripts/test_script_tests_gate.py 0.9s rc=0
2026-10-26T13:49:17.0759648Z OK scripts/test_security_scan_gate.py 16.7s rc=0
2026-10-26T13:49:17.0759885Z OK scripts/test_status_view_gate.py 5.7s rc=0
2026-10-26T13:49:17.0760140Z OK scripts/test_tenantsync_prune.py 0.1s rc=0
2026-10-26T13:49:17.0760413Z OK scripts/test_tenantsync_selfcheck.py 0.2s rc=0
2026-10-26T13:49:17.0760694Z OK scripts/bench/test_plugcut.py 0.2s rc=0
2026-10-26T13:49:17.0760943Z OK scripts/bench/test_plugswitch.py 0.2s rc=0
2026-10-26T13:49:17.0761189Z OK scripts/dooplex-offsite/test_dooplex_offsite.py 68.5s rc=0
2026-10-26T13:49:17.0761482Z OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 3.5s rc=0
2026-10-26T13:49:17.0761739Z OK scripts/facebook/test_fb_probe.py 0.4s rc=0
2026-10-26T13:49:17.0762015Z OK scripts/hub-db-backup/test_hub_db_backup.py 14.0s rc=0
2026-10-26T13:49:17.0762307Z OK scripts/iso/test/test_bound_banner_clears.py 0.1s rc=0
2026-10-26T13:49:17.0762538Z OK scripts/iso/test/test_console_glyphs.py 0.1s rc=0
2026-10-26T13:49:17.0762801Z OK scripts/iso/test/test_rootpw_guard.py 0.2s rc=0
2026-10-26T13:49:17.0763104Z OK scripts/release-copy/test_release_copy.py 0.5s rc=0
2026-10-26T13:49:17.0763407Z script-tests: OK — 35 suite(s), every one exited 0
2026-10-26T13:49:17.0781930Z == gate: decoy-coverage (decoy_coverage_gate.py /root/.cache/act/cdd17f98aeae57bd/hostexecutor /root/.cache/act/cdd17f98aeae57bd/felhom-controller /root/.cache/act/cdd17
2026-10-26T13:49:17.6516884Z == gate: security-scan (security_scan_gate.py /root/.cache/act/cdd17f98aeae57bd/hostexecutor)
2026-10-26T13:51:06.6220333Z security-scan: 10 new hit(s), 42 accepted — warn-first, the push goes on
@@ -0,0 +1,40 @@
2026-10-26T17:12:36.5594913Z == gate: script-tests (script_tests_gate.py /root/.cache/act/e82d471840c0952e/hostexecutor)
2026-10-26T17:16:39.1744210Z OK scripts/test_build_sizing_web.py 0.2s rc=0
2026-10-26T17:16:39.1745588Z OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
2026-10-26T17:16:39.1745890Z OK scripts/test_due_checks_gate.py 38.3s rc=0
2026-10-26T17:16:39.1746163Z OK scripts/test_felhom_restore_beside.py 0.6s rc=0
2026-10-26T17:16:39.1746490Z OK scripts/test_gate_decoys.py 89.9s rc=0
2026-10-26T17:16:39.1746772Z OK scripts/test_golden_currency_gate.py 1.4s rc=0
2026-10-26T17:16:39.1747079Z OK scripts/test_guide_quote_gate.py 0.5s rc=0
2026-10-26T17:16:39.1747586Z OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
2026-10-26T17:16:39.1747831Z OK scripts/test_hostinstall.py 0.8s rc=0
2026-10-26T17:16:39.1748125Z OK scripts/test_hu_grep.py 0.1s rc=0
2026-10-26T17:16:39.1748354Z OK scripts/test_hub_copy_drift.py 0.1s rc=0
2026-10-26T17:16:39.1748579Z OK scripts/test_instructions_gate.py 2.1s rc=0
2026-10-26T17:16:39.1748923Z OK scripts/test_iso_bootstrap_gate.py 1.1s rc=0
2026-10-26T17:16:39.1749157Z OK scripts/test_no_latest_push.py 0.1s rc=0
2026-10-26T17:16:39.1749396Z OK scripts/test_push_scope.py 0.1s rc=0
2026-10-26T17:16:39.1749660Z OK scripts/test_read_credential.py 0.1s rc=0
2026-10-26T17:16:39.1749895Z OK scripts/test_repo_gates.py 37.6s rc=0
2026-10-26T17:16:39.1750116Z OK scripts/test_repo_gates_docstring.py 0.1s rc=0
2026-10-26T17:16:39.1750381Z OK scripts/test_repo_gates_scope.py 0.4s rc=0
2026-10-26T17:16:39.1750758Z OK scripts/test_reuse_refs_check.py 0.3s rc=0
2026-10-26T17:16:39.1750986Z OK scripts/test_script_tests_gate.py 0.5s rc=0
2026-10-26T17:16:39.1751248Z OK scripts/test_security_scan_gate.py 6.8s rc=0
2026-10-26T17:16:39.1751480Z OK scripts/test_status_view_gate.py 2.3s rc=0
2026-10-26T17:16:39.1751694Z OK scripts/test_tenantsync_prune.py 0.1s rc=0
2026-10-26T17:16:39.1751957Z OK scripts/test_tenantsync_selfcheck.py 0.1s rc=0
2026-10-26T17:16:39.1752243Z OK scripts/bench/test_plugcut.py 0.1s rc=0
2026-10-26T17:16:39.1752541Z OK scripts/bench/test_plugswitch.py 0.1s rc=0
2026-10-26T17:16:39.1752820Z OK scripts/dooplex-offsite/test_dooplex_offsite.py 48.6s rc=0
2026-10-26T17:16:39.1753036Z OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.5s rc=0
2026-10-26T17:16:39.1753269Z OK scripts/facebook/test_fb_probe.py 0.2s rc=0
2026-10-26T17:16:39.1753526Z OK scripts/hub-db-backup/test_hub_db_backup.py 8.1s rc=0
2026-10-26T17:16:39.1753762Z OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
2026-10-26T17:16:39.1753979Z OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
2026-10-26T17:16:39.1754267Z OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
2026-10-26T17:16:39.1754491Z OK scripts/release-copy/test_release_copy.py 0.2s rc=0
2026-10-26T17:16:39.1754729Z script-tests: OK — 35 suite(s), every one exited 0
2026-10-26T17:16:39.1885982Z == gate: decoy-coverage (decoy_coverage_gate.py /root/.cache/act/e82d471840c0952e/hostexecutor /root/.cache/act/e82d471840c0952e/felhom-controller /root/.cache/act/e82d4
2026-10-26T17:16:39.4762840Z == gate: security-scan (security_scan_gate.py /root/.cache/act/e82d471840c0952e/hostexecutor)
2026-10-26T17:17:06.0127522Z security-scan: 10 new hit(s), 42 accepted — warn-first, the push goes on
@@ -0,0 +1,41 @@
2026-10-26T17:18:09.7889931Z suite filter: ON (a branch push with a known file list)
2026-10-26T17:18:18.4276734Z == gate: script-tests (script_tests_gate.py /root/.cache/act/f015d6564ba3139c/hostexecutor)
2026-10-26T17:20:21.8188080Z OK scripts/test_build_sizing_web.py 0.2s rc=0
2026-10-26T17:20:21.8189127Z OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
2026-10-26T17:20:21.8189544Z OK scripts/test_due_checks_gate.py 0.9s rc=0
2026-10-26T17:20:21.8189909Z OK scripts/test_felhom_restore_beside.py 0.5s rc=0
2026-10-26T17:20:21.8190332Z OK scripts/test_gate_decoys.py 90.1s rc=0
2026-10-26T17:20:21.8190886Z OK scripts/test_golden_currency_gate.py 1.4s rc=0
2026-10-26T17:20:21.8191212Z OK scripts/test_guide_quote_gate.py 0.5s rc=0
2026-10-26T17:20:21.8191644Z OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
2026-10-26T17:20:21.8191975Z OK scripts/test_hostinstall.py 0.8s rc=0
2026-10-26T17:20:21.8192218Z OK scripts/test_hu_grep.py 0.1s rc=0
2026-10-26T17:20:21.8192480Z OK scripts/test_hub_copy_drift.py 0.1s rc=0
2026-10-26T17:20:21.8192723Z OK scripts/test_instructions_gate.py 2.1s rc=0
2026-10-26T17:20:21.8192961Z OK scripts/test_iso_bootstrap_gate.py 1.1s rc=0
2026-10-26T17:20:21.8193205Z OK scripts/test_no_latest_push.py 0.1s rc=0
2026-10-26T17:20:21.8193467Z OK scripts/test_push_scope.py 0.1s rc=0
2026-10-26T17:20:21.8193700Z OK scripts/test_read_credential.py 0.1s rc=0
2026-10-26T17:20:21.8193927Z OK scripts/test_repo_gates.py 3.5s rc=0
2026-10-26T17:20:21.8194139Z OK scripts/test_repo_gates_docstring.py 0.0s rc=0
2026-10-26T17:20:21.8194382Z OK scripts/test_repo_gates_scope.py 0.4s rc=0
2026-10-26T17:20:21.8194624Z OK scripts/test_reuse_refs_check.py 0.2s rc=0
2026-10-26T17:20:21.8194894Z OK scripts/test_script_tests_gate.py 1.4s rc=0
2026-10-26T17:20:21.8195299Z OK scripts/test_security_scan_gate.py 6.7s rc=0
2026-10-26T17:20:21.8195534Z OK scripts/test_status_view_gate.py 2.3s rc=0
2026-10-26T17:20:21.8195829Z OK scripts/test_tenantsync_prune.py 0.1s rc=0
2026-10-26T17:20:21.8196204Z OK scripts/test_tenantsync_selfcheck.py 0.1s rc=0
2026-10-26T17:20:21.8196478Z OK scripts/bench/test_plugcut.py 0.1s rc=0
2026-10-26T17:20:21.8196727Z OK scripts/bench/test_plugswitch.py 0.1s rc=0
2026-10-26T17:20:21.8196990Z SKIP scripts/dooplex-offsite/test_dooplex_offsite.py none of its paths (scripts/dooplex-offsite/) changed in this push — it runs on a release tag and nightly
2026-10-26T17:20:21.8197523Z OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.5s rc=0
2026-10-26T17:20:21.8197782Z OK scripts/facebook/test_fb_probe.py 0.2s rc=0
2026-10-26T17:20:21.8198009Z OK scripts/hub-db-backup/test_hub_db_backup.py 8.0s rc=0
2026-10-26T17:20:21.8198222Z OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
2026-10-26T17:20:21.8198521Z OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
2026-10-26T17:20:21.8198736Z OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
2026-10-26T17:20:21.8198950Z OK scripts/release-copy/test_release_copy.py 0.2s rc=0
2026-10-26T17:20:21.8199183Z script-tests: OK — 34 suite(s) ran, every one exited 0, 1 skipped by path (R-1297)
2026-10-26T17:20:21.8329076Z == gate: decoy-coverage (decoy_coverage_gate.py /root/.cache/act/f015d6564ba3139c/hostexecutor /root/.cache/act/f015d6564ba3139c/felhom-controller /root/.cache/act/f015d
2026-10-26T17:20:22.1007937Z == gate: security-scan (security_scan_gate.py /root/.cache/act/f015d6564ba3139c/hostexecutor)
2026-10-26T17:20:50.4881408Z security-scan: 10 new hit(s), 42 accepted — warn-first, the push goes on
@@ -0,0 +1,41 @@
2026-10-26T17:28:47.7729918Z suite filter: ON (a branch push with a known file list)
2026-10-26T17:28:56.1037450Z == gate: script-tests (script_tests_gate.py /root/.cache/act/cc3bec12ce9ea276/hostexecutor)
2026-10-26T17:29:28.5067637Z OK scripts/test_build_sizing_web.py 0.2s rc=0
2026-10-26T17:29:28.5068146Z OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
2026-10-26T17:29:28.5068305Z SKIP scripts/test_due_checks_gate.py none of its paths (scripts/due_checks_gate.py, scripts/test_due_checks_gate.py, scripts/repo_gates.py, scrip
2026-10-26T17:29:28.5068488Z OK scripts/test_felhom_restore_beside.py 0.5s rc=0
2026-10-26T17:29:28.5069032Z SKIP scripts/test_gate_decoys.py none of its paths (scripts/, .gitea/, .githooks/) changed in this push — it runs on a release tag and nigh
2026-10-26T17:29:28.5069271Z OK scripts/test_golden_currency_gate.py 1.4s rc=0
2026-10-26T17:29:28.5069398Z OK scripts/test_guide_quote_gate.py 0.5s rc=0
2026-10-26T17:29:28.5069518Z OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
2026-10-26T17:29:28.5069622Z OK scripts/test_hostinstall.py 0.8s rc=0
2026-10-26T17:29:28.5069725Z OK scripts/test_hu_grep.py 0.1s rc=0
2026-10-26T17:29:28.5069822Z OK scripts/test_hub_copy_drift.py 0.1s rc=0
2026-10-26T17:29:28.5069925Z OK scripts/test_instructions_gate.py 2.1s rc=0
2026-10-26T17:29:28.5070020Z OK scripts/test_iso_bootstrap_gate.py 1.1s rc=0
2026-10-26T17:29:28.5070167Z OK scripts/test_no_latest_push.py 0.1s rc=0
2026-10-26T17:29:28.5070276Z OK scripts/test_push_scope.py 0.1s rc=0
2026-10-26T17:29:28.5070379Z OK scripts/test_read_credential.py 0.1s rc=0
2026-10-26T17:29:28.5070499Z OK scripts/test_repo_gates.py 3.4s rc=0
2026-10-26T17:29:28.5070600Z OK scripts/test_repo_gates_docstring.py 0.1s rc=0
2026-10-26T17:29:28.5070702Z OK scripts/test_repo_gates_scope.py 0.4s rc=0
2026-10-26T17:29:28.5070806Z OK scripts/test_reuse_refs_check.py 0.3s rc=0
2026-10-26T17:29:28.5070977Z OK scripts/test_script_tests_gate.py 1.4s rc=0
2026-10-26T17:29:28.5072435Z OK scripts/test_security_scan_gate.py 6.8s rc=0
2026-10-26T17:29:28.5072830Z OK scripts/test_status_view_gate.py 2.3s rc=0
2026-10-26T17:29:28.5073074Z OK scripts/test_tenantsync_prune.py 0.1s rc=0
2026-10-26T17:29:28.5073299Z OK scripts/test_tenantsync_selfcheck.py 0.1s rc=0
2026-10-26T17:29:28.5073586Z OK scripts/bench/test_plugcut.py 0.1s rc=0
2026-10-26T17:29:28.5073872Z OK scripts/bench/test_plugswitch.py 0.1s rc=0
2026-10-26T17:29:28.5074106Z SKIP scripts/dooplex-offsite/test_dooplex_offsite.py none of its paths (scripts/dooplex-offsite/) changed in this push — it runs on a release tag and nightly
2026-10-26T17:29:28.5074484Z OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.6s rc=0
2026-10-26T17:29:28.5074721Z OK scripts/facebook/test_fb_probe.py 0.2s rc=0
2026-10-26T17:29:28.5074931Z OK scripts/hub-db-backup/test_hub_db_backup.py 8.0s rc=0
2026-10-26T17:29:28.5075217Z OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
2026-10-26T17:29:28.5075436Z OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
2026-10-26T17:29:28.5075680Z OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
2026-10-26T17:29:28.5075908Z OK scripts/release-copy/test_release_copy.py 0.2s rc=0
2026-10-26T17:29:28.5076184Z script-tests: OK — 32 suite(s) ran, every one exited 0, 3 skipped by path (R-1297)
2026-10-26T17:29:28.5150602Z == gate: decoy-coverage (decoy_coverage_gate.py /root/.cache/act/cc3bec12ce9ea276/hostexecutor /root/.cache/act/cc3bec12ce9ea276/felhom-controller /root/.cache/act/cc3be
2026-10-26T17:29:28.7683218Z == gate: security-scan (security_scan_gate.py /root/.cache/act/cc3bec12ce9ea276/hostexecutor)
2026-10-26T17:29:59.0587029Z security-scan: 10 new hit(s), 42 accepted — warn-first, the push goes on
@@ -0,0 +1,63 @@
register-shape: CONVICTED — 1 problem(s)
line 402 R-1297: RULE 5 — 6 cells where the header declares 8. A literal `|` outside backticks makes a cell; put it inside `code`.
OK scripts/test_build_sizing_web.py 0.1s rc=0
OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
SKIP scripts/test_due_checks_gate.py none of its paths (scripts/due_checks_gate.py, scripts/test_due_checks_gate.py, scripts/repo_gates.py, scripts/script_tests_gate.py) changed in this push —
OK scripts/test_felhom_restore_beside.py 0.5s rc=0
SKIP scripts/test_gate_decoys.py none of its paths (scripts/, .gitea/, .githooks/) changed in this push — it runs on a release tag and nightly
OK scripts/test_golden_currency_gate.py 1.0s rc=0
OK scripts/test_guide_quote_gate.py 0.3s rc=0
OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
OK scripts/test_hostinstall.py 0.7s rc=0
OK scripts/test_hu_grep.py 0.0s rc=0
OK scripts/test_hub_copy_drift.py 0.0s rc=0
OK scripts/test_instructions_gate.py 1.6s rc=0
OK scripts/test_iso_bootstrap_gate.py 0.8s rc=0
OK scripts/test_no_latest_push.py 0.1s rc=0
OK scripts/test_push_scope.py 0.0s rc=0
OK scripts/test_read_credential.py 0.1s rc=0
OK scripts/test_repo_gates.py 2.8s rc=0
OK scripts/test_repo_gates_docstring.py 0.0s rc=0
OK scripts/test_repo_gates_scope.py 0.3s rc=0
OK scripts/test_reuse_refs_check.py 0.1s rc=0
OK scripts/test_script_tests_gate.py 1.1s rc=0
OK scripts/test_security_scan_gate.py 6.1s rc=0
OK scripts/test_status_view_gate.py 1.7s rc=0
OK scripts/test_tenantsync_prune.py 0.7s rc=0
OK scripts/test_tenantsync_selfcheck.py 1.1s rc=0
OK scripts/bench/test_plugcut.py 0.1s rc=0
OK scripts/bench/test_plugswitch.py 0.1s rc=0
SKIP scripts/dooplex-offsite/test_dooplex_offsite.py none of its paths (scripts/dooplex-offsite/) changed in this push — it runs on a release tag and nightly
OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.2s rc=0
OK scripts/facebook/test_fb_probe.py 0.1s rc=0
OK scripts/hub-db-backup/test_hub_db_backup.py 5.7s rc=0
OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
OK scripts/release-copy/test_release_copy.py 0.2s rc=0
site OK (exit 0)
hostinstall OK (exit 0)
hostinstall-harness OK (exit 0)
hub-confirm OK (exit 0)
manifest-bearer OK (exit 0)
reuse-refs OK (exit 0)
instructions OK (exit 0)
golden-currency OK (exit 0)
wire-contract OK (exit 0)
hub-copy OK (exit 0)
guide-quote OK (exit 0)
host-swap-rule OK (exit 0)
due-checks OK (exit 0)
one-register OK (exit 0)
closed-register OK (exit 0)
observations OK (exit 0)
register-shape FAILED (exit 1)
stands OK (exit 0)
script-tests OK (exit 0)
decoy-coverage OK (exit 0)
poster-facts OK (exit 0)
hub-gofmt OK (exit 0)
security-scan OK (exit 0)
status-view OK (exit 0)
CONVICTED: register-shape
decoy1 rc=1
@@ -0,0 +1,60 @@
OK scripts/test_build_sizing_web.py 0.1s rc=0
OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
SKIP scripts/test_due_checks_gate.py none of its paths (scripts/due_checks_gate.py, scripts/test_due_checks_gate.py, scripts/repo_gates.py, scripts/script_tests_gate.py) changed in this push —
OK scripts/test_felhom_restore_beside.py 0.5s rc=0
OK scripts/test_gate_decoys.py 82.5s rc=0
OK scripts/test_golden_currency_gate.py 1.1s rc=0
OK scripts/test_guide_quote_gate.py 0.4s rc=0
OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
OK scripts/test_hostinstall.py 0.7s rc=0
OK scripts/test_hu_grep.py 0.0s rc=0
OK scripts/test_hub_copy_drift.py 0.0s rc=0
OK scripts/test_instructions_gate.py 1.6s rc=0
OK scripts/test_iso_bootstrap_gate.py 0.8s rc=0
OK scripts/test_no_latest_push.py 0.1s rc=0
OK scripts/test_push_scope.py 0.0s rc=0
OK scripts/test_read_credential.py 0.1s rc=0
OK scripts/test_repo_gates.py 2.7s rc=0
OK scripts/test_repo_gates_docstring.py 0.0s rc=0
OK scripts/test_repo_gates_scope.py 0.3s rc=0
OK scripts/test_reuse_refs_check.py 0.1s rc=0
OK scripts/test_script_tests_gate.py 1.2s rc=0
OK scripts/test_security_scan_gate.py 6.3s rc=0
OK scripts/test_status_view_gate.py 1.7s rc=0
OK scripts/test_tenantsync_prune.py 0.7s rc=0
OK scripts/test_tenantsync_selfcheck.py 1.0s rc=0
OK scripts/bench/test_plugcut.py 0.1s rc=0
OK scripts/bench/test_plugswitch.py 0.1s rc=0
SKIP scripts/dooplex-offsite/test_dooplex_offsite.py none of its paths (scripts/dooplex-offsite/) changed in this push — it runs on a release tag and nightly
OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.2s rc=0
OK scripts/facebook/test_fb_probe.py 0.1s rc=0
OK scripts/hub-db-backup/test_hub_db_backup.py 5.6s rc=0
OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
OK scripts/release-copy/test_release_copy.py 0.2s rc=0
site OK (exit 0)
hostinstall OK (exit 0)
hostinstall-harness OK (exit 0)
hub-confirm OK (exit 0)
manifest-bearer OK (exit 0)
reuse-refs OK (exit 0)
instructions OK (exit 0)
golden-currency OK (exit 0)
wire-contract OK (exit 0)
hub-copy OK (exit 0)
guide-quote OK (exit 0)
host-swap-rule OK (exit 0)
due-checks OK (exit 0)
one-register OK (exit 0)
closed-register OK (exit 0)
observations OK (exit 0)
register-shape OK (exit 0)
stands OK (exit 0)
script-tests OK (exit 0)
decoy-coverage OK (exit 0)
poster-facts OK (exit 0)
hub-gofmt OK (exit 0)
security-scan OK (exit 0)
status-view OK (exit 0)
decoy2 rc=0
@@ -0,0 +1,63 @@
OK scripts/test_build_sizing_web.py 0.1s rc=0
OK scripts/test_dockerfile_arg_order.py 0.0s rc=0
SKIP scripts/test_due_checks_gate.py none of its paths (scripts/due_checks_gate.py, scripts/test_due_checks_gate.py, scripts/repo_gates.py, scripts/script_tests_gate.py) changed in this push —
OK scripts/test_felhom_restore_beside.py 0.5s rc=0
FAIL scripts/test_gate_decoys.py 83.2s rc=1
| FAIL: register-shape [pipe-outside-backticks]: the SCRIPT exited 0, want 1 — check() and the entry point disagree
| 9 decoy(s) of 95 exposed a hole
OK scripts/test_golden_currency_gate.py 1.1s rc=0
OK scripts/test_guide_quote_gate.py 0.3s rc=0
OK scripts/test_host_swap_rule_gate.py 0.2s rc=0
OK scripts/test_hostinstall.py 0.6s rc=0
OK scripts/test_hu_grep.py 0.0s rc=0
OK scripts/test_hub_copy_drift.py 0.0s rc=0
OK scripts/test_instructions_gate.py 1.5s rc=0
OK scripts/test_iso_bootstrap_gate.py 0.8s rc=0
OK scripts/test_no_latest_push.py 0.1s rc=0
OK scripts/test_push_scope.py 0.0s rc=0
OK scripts/test_read_credential.py 0.1s rc=0
OK scripts/test_repo_gates.py 2.9s rc=0
OK scripts/test_repo_gates_docstring.py 0.0s rc=0
OK scripts/test_repo_gates_scope.py 0.3s rc=0
OK scripts/test_reuse_refs_check.py 0.1s rc=0
OK scripts/test_script_tests_gate.py 1.2s rc=0
OK scripts/test_security_scan_gate.py 6.1s rc=0
OK scripts/test_status_view_gate.py 1.7s rc=0
OK scripts/test_tenantsync_prune.py 0.7s rc=0
OK scripts/test_tenantsync_selfcheck.py 1.0s rc=0
OK scripts/bench/test_plugcut.py 0.1s rc=0
OK scripts/bench/test_plugswitch.py 0.1s rc=0
SKIP scripts/dooplex-offsite/test_dooplex_offsite.py none of its paths (scripts/dooplex-offsite/) changed in this push — it runs on a release tag and nightly
OK scripts/ep0-copy-gc/test_ep0_copy_gc.py 1.1s rc=0
OK scripts/facebook/test_fb_probe.py 0.1s rc=0
OK scripts/hub-db-backup/test_hub_db_backup.py 5.7s rc=0
OK scripts/iso/test/test_bound_banner_clears.py 0.0s rc=0
OK scripts/iso/test/test_console_glyphs.py 0.0s rc=0
OK scripts/iso/test/test_rootpw_guard.py 0.1s rc=0
OK scripts/release-copy/test_release_copy.py 0.2s rc=0
site OK (exit 0)
hostinstall OK (exit 0)
hostinstall-harness OK (exit 0)
hub-confirm OK (exit 0)
manifest-bearer OK (exit 0)
reuse-refs OK (exit 0)
instructions OK (exit 0)
golden-currency OK (exit 0)
wire-contract OK (exit 0)
hub-copy OK (exit 0)
guide-quote OK (exit 0)
host-swap-rule OK (exit 0)
due-checks OK (exit 0)
one-register OK (exit 0)
closed-register OK (exit 0)
observations OK (exit 0)
register-shape OK (exit 0)
stands OK (exit 0)
script-tests FAILED (exit 1)
decoy-coverage OK (exit 0)
poster-facts OK (exit 0)
hub-gofmt OK (exit 0)
security-scan OK (exit 0)
status-view OK (exit 0)
CONVICTED: script-tests
decoy2b rc=1
+2 -1
View File
@@ -305,7 +305,7 @@ stopping line that lies.
| **R-1223** | Monitoring & notifications | P3 | **After a reboot, a box whose tunnel ep0 refuses is invisible: the future stamp is gone, so neither the D30 re-key nor the hub's alarm sees it.** Fresh review 2026-10-23 night (`REPORT-night-2026-10-23.md`, A8 + H1): the kernel forgets `latest-handshake` when the interface restarts (0 = never); the agent then sends neither `last_handshake_age_s` nor `handshake_future_s`, and `checkWGTunnel` (`hub/internal/api/host_tunnel.go`) does not judge a registered, active tunnel with no handshake at all. Shape: the clock ran ahead, was corrected, the box rebooted (some households switch off at night), ep0 still holds the future stamp and starts nothing. | OPEN | — | Agent: report `tunnel_up_s` (how long the unit has been active) with no handshake; hub: a registered, active tunnel with no handshake for > 30 min while reporting → `wg_tunnel_stale`; the agent re-keys the same way after 30 min with no handshake. Tests both sides; rides an agent release + a hub deploy | CC |
| **R-1224** | Monitoring & notifications | P4 | **The operator's 1-hour mail cooldown for `wg_tunnel_stale` is per customer, not per host, and `wg_tunnel_ok` does not end it.** Fresh review 2026-10-23 night (`REPORT-night-2026-10-23.md`, H3): `notify/dispatcher.go:620` keys the cooldown `customerID:eventType`. Two hosts of one household going stale within an hour, or stale → ok → stale, store a second event row but mail once (the suppression is in `notification_log`). | OPEN | — | Key the cooldown by `host_id` from the details for host-scoped operator events, and let the matching ok event end it; a dispatcher test with two hosts; rides a hub deploy | CC |
| **R-1233** | Monitoring & notifications | P4 | **`[ERROR] [stacks] execCommand failed: exit status 1` names no command.** Seen once a night (2026-10-21 00:30:49 and 2026-10-23 02:17:14 UTC, each right after an app stopped for its dump); the command and stderr go only into the returned error, which the caller may drop (`internal/stacks/manager.go:1757`). An ERROR nobody can judge. | OPEN | — | Log the command's first two words (`docker inspect`, never the arguments — `docker exec … psql` can carry a password) and the stderr's first line; find the caller of the nightly one | CC |
| **R-1291** | Monitoring & notifications | P3 | **A held app gives the operator no lasting signal: one event at the stop, then nothing — demo-hp's Kimai sat held 11 days unseen.** Found 2026-10-25 night (R-1286). No held-app state reaches the hub (the report carries none; the hub shows none); the crash-loop stop sends `app_stopped_unhealthy` once. On a household box an app held after a crash loop, a failed update or a failed restore stays stopped until someone opens its page. | **OPEN** — ruled 2026-10-26 evening: **D53 = operator-only reminders first** (`09` §3 decision 265); CC builds `08` §10, commit only. **Earlier:** WAITING-ON-OPERATOR — 2026-10-26: the design is written (`08` §10 [DESIGN — proposed]: `app_held` / `app_hold_cleared` events + a daily re-send, a „Held apps" list on the host page, an operator reminder after a grace per reason, the hub never clears a hold; ~½ day controller + ~1 day hub). The pick is STATUS D53. **Earlier:** OPEN | — | a two-repo design: the controller reports its holds (stack, reason, since), the hub shows them on the host and mails a reminder after N days (the D-level of the reason); one row's worth of design first | CC |
| **R-1291** | Monitoring & notifications | P3 | **A held app gives the operator no lasting signal: one event at the stop, then nothing — demo-hp's Kimai sat held 11 days unseen.** Found 2026-10-25 night (R-1286). No held-app state reaches the hub (the report carries none; the hub shows none); the crash-loop stop sends `app_stopped_unhealthy` once. On a household box an app held after a crash loop, a failed update or a failed restore stays stopped until someone opens its page. | **OPEN — BUILT, commit only, NOT released** — 2026-10-26 evening: controller `5bd03ad` (`internal/holdreport`) + hub `7e57e824` (`held_apps`, `HeldAppsChecker`, the „Held apps" list on the host page and the Hosts page); red-proved; live on 9202 against a test hub with a 3-min grace (listed, one operator mail, Start cleared it, the row went, household leg `skipped operator_only`) — `audits/d53-held-apps-2026-10-26/`. Closes when both release. Ruled D53 = operator-only reminders first (`09` §3 decision 265). **Earlier:** WAITING-ON-OPERATOR — 2026-10-26: the design is written (`08` §10 [DESIGN — proposed]: `app_held` / `app_hold_cleared` events + a daily re-send, a „Held apps" list on the host page, an operator reminder after a grace per reason, the hub never clears a hold; ~½ day controller + ~1 day hub). The pick is STATUS D53. **Earlier:** OPEN | — | a two-repo design: the controller reports its holds (stack, reason, since), the hub shows them on the host and mails a reminder after N days (the D-level of the reason); one row's worth of design first | CC |
| **R-1296** | Monitoring & notifications | P4 | **When ep0's tenancy script refuses, the hub log shows only its one-line error — PBS's own reason (the 403 `missing Datastore.Audit|Datastore.Backup on …`) stays in the script's stderr, which the hub drops on a refusal.** Found 2026-10-26 diagnosing R-1295: the cause was visible only by reading ep0's proxy journal over root SSH. `hub/internal/tenantsync/client.go` `parseResponse` returns the JSON error and ignores stderr; the script keeps secrets off stderr by design (its header), so the tail is safe to log. | OPEN | — | append the last ~3 stderr lines to the refusal error (bounded, never stdout); a test that a secret on stdout never appears | CC |
## Hub & operator — 12 rows (P3 3, P4 9)
@@ -400,3 +400,4 @@ stopping line that lies.
| R-1035 | 2026-12-09 | the same three numbers 8 weeks after publishing, compared with the 4-week read; a null at 4 weeks is not a verdict, both reads are needed; detail in the R-1035 row |
<!-- DUE-CHECKS-END -->
| **R-1297** | Process & tooling | P3 | **CI waits dominate a working day: felhom.eu's CI run takes ~13 min and the ONE runner makes every repo's job wait behind it (up to 10–17 min queued).** Measured 2026-10-26 (operator: „these CI waiting times are getting out of hand"): since 10-25 felhom.eu runs median 793 s, the controller/agent/catalog 57–102 s; queue median 5 s, max 548–1023 s. In run 2366, ~10 of 13 minutes are four suites — `test_gate_decoys.py` 232 s, `test_due_checks_gate.py` 133 s, `test_repo_gates.py` 132 s (it runs the whole gate set a SECOND time), `dooplex-offsite/test_dooplex_offsite.py` 69 s — plus `hub-gofmt` ~110 s (a cold Go cache per run). **Ruled 2026-10-26 by the operator: BOTH** — (A) trim felhom.eu CI: the heavy suites only when their own files change, no duplicate full run, a Go cache for hub-gofmt; (B) a second CI runner on DooPlex. | OPEN — (B) DONE by the operator 2026-10-26: a second runner, both runners 2 CPUs (`09` §3 decision 267); (A) CC, 2026-10-26 evening | — | (A) CC, a run of its own (a CI scope change, ruled); (B) a DooPlex change — a brief or the homelab session; and CC waits on CI only before a release or at the end of a run | CC |
| **R-1298** | Monitoring & notifications | P3 | **Scratch guest 9202 carries leftovers made outside the product, and D53 would remind about them every week once released: four crash-loop holds since 2026-09-24 for apps that are not installed (`chaoscrash`, `chaosoom`, `chaosoomb`, `gokapi`), and a PrivateBin `app.yaml` (2026-10-25) with no container that the product reads as „installed".** Found 2026-10-26 evening in the D53 live proof: the new reporter sent the four holds at its first tick and the test hub mailed four reminders („held 32 days"). The product's removal clears an install's hold (R-704); these apps were removed by hand in a chaos drill. On a household box the same shape would mail the operator weekly about an app that does not exist. | OPEN | — | decide whether the reporter (or the hub) skips an `unhealthy_stop` / `update_failed` / `putback_pending` hold whose app is not installed (a restore hold stays reported — operator-cleared by design, R-379); then clear 9202's four holds with `-clear-restore-hold` and remove PrivateBin through the product | CC |
+13
View File
@@ -1,3 +1,16 @@
## 2026-10-26 (evening) — CI: heavy suites by path, no second full gate run, a nightly run (R-1297 A); a decoy hole closed
- **`script_tests_gate.py` `PATH_FILTERED`:** on a CI branch push with a known file list, `test_gate_decoys.py`,
`test_due_checks_gate.py` and `dooplex-offsite/test_dooplex_offsite.py` run only when one of their paths changed; no
list (a tag, the nightly run, a dispatch, the pre-push hook), an empty or unreadable list = every suite (`ecd82aba`).
- **`.gitea/workflows/gates.yml`:** a nightly schedule 03:20 UTC; `FELHOM_CHANGED_FILES` only for `push:refs/heads/*`; a
trimmed commits array is an unknown list. Pinned by `test_script_tests_gate.py` (filter cases + workflow pins).
- **`test_repo_gates.py` and `test_due_checks_gate.py` group G** drive the real runner with `GATES` narrowed by label —
the wiring, not the whole gate set a second time (~130 s each in CI).
- **`test_gate_decoys.py` register-shape cases assert the SCRIPT's exit code too.** Found by R-1297's decoy: an inert
`main()` passed every case because the cases called `check()` only; red-proved with that inert `main()` (9 cases red).
- Numbers and decoys: `documentation/audits/r1297-ci-trim-2026-10-26/`.
## 2026-10-26 — felhom-tenantsync v1.4.0: the self-check waits out the PBS permission cache (R-1295) — INSTALLED on ep0 2026-10-26 13:09Z on the operator's yes (sha256 `751b3c26…` = the repository's; v1.3.0 kept as `.v1.3.0`; `fingerprint` and `usage` ok)
- **`felhom-tenantsync.sh` v1.4.0:** the new token is re-checked every 0.5 s for up to 15 s (a log line per try) before
+9 -1
View File
@@ -882,7 +882,15 @@ def _rsg_case(name, text, must_convict, expect_rule=None):
fh.write(text)
rows, ids, bad = _rsg.check(fp)
convicted = bool(bad)
if convicted != must_convict:
# 2026-10-26 (R-1297 A's decoy): the RUNNER calls the script, not check(). An inert main() (convicting
# nothing) passed every case here while check() still worked — so the script's own exit code is
# asserted too, on every case.
_p = subprocess.run([sys.executable, os.path.join(ROOT, "scripts", "register_shape_gate.py"), fp],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if _p.returncode != (1 if must_convict else 0):
fails.append("register-shape [%s]: the SCRIPT exited %d, want %d — check() and the entry point disagree"
% (name, _p.returncode, 1 if must_convict else 0))
elif convicted != must_convict:
fails.append("register-shape [%s]: expected %s, got %s (%s)"
% (name, "CONVICT" if must_convict else "PASS",
"CONVICT" if convicted else "PASS",