app page tuning round 2: P0 cause (Calibre-Web installed before its password field), controller 0.319.0 released, 02 layout rules (style A, login panel, empty-value rule), R-1095 opened, STATUS
gates / gates (push) Successful in 6m54s
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
@@ -0,0 +1,38 @@
|
||||
# REPORT — app page tuning, round 2 (2026-10-16)
|
||||
|
||||
Written in ASD-STE100 Simplified Technical English. The full technical report is felhom-controller `REPORT.md`
|
||||
(v0.319.0). Evidence: `documentation/audits/app-page-tuning-2026-10-16/` (P0, before/, browser/, release/, tools/,
|
||||
redproof.txt). Design read and updated: `architecture/02-controller-module-map.md` „The app page's layout".
|
||||
|
||||
## 1. Why the Calibre-Web password was empty (Part 0)
|
||||
|
||||
Calibre-Web on demo-hp was installed on 2026-08-21. Its template got the password field on 2026-09-29. The box adds new
|
||||
generated values to installed apps, but never a password. So the box never had one. The user name the box shows is the
|
||||
real one: a session renamed Calibre-Web's admin to it on 2026-10-01. Nothing was removed. No customer box has this state
|
||||
(the hub lists only demo-hp and demo-felhom; demo-felhom has no Calibre-Web now — checked on the box).
|
||||
|
||||
## 2. What changed on the page
|
||||
|
||||
The header, the tab bar and every section are panels (style A). The open tab is filled and underlined. One
|
||||
„Belépés az alkalmazásba" panel shows the name and the password side by side. A value the box does not have is a
|
||||
sentence, not an eye. „Cím és kulcsok" is rows: no Domain input, no storage list. The install page uses the same panels;
|
||||
its fields did not change. Checked in a real (headless) Chrome through `https://felhom.enkisfelhom.hu`.
|
||||
|
||||
Login pairs: calibre-web, kimai, lubelogger, nextcloud, paperless-ngx, radicale. 14 other apps show the password alone.
|
||||
|
||||
## 3. Release
|
||||
|
||||
Controller **0.319.0**: Tester 1, demo-hp and demo-felhom took it through their own floor at 10:34 UTC; box walk checks
|
||||
green on all three; global floor 0.319.0 / MinAgent **0.158.1** (first saved with 0.157.0 by my mistake, corrected 65
|
||||
seconds later; no box was affected — see the controller report §6). Per-customer floors cleared.
|
||||
|
||||
## 4. Register
|
||||
|
||||
Rows before 174, after 175; opened 1 (R-1095: `/apps/{slug}/install` for an installed app), closed 0.
|
||||
|
||||
## 5. Other session duties
|
||||
|
||||
- Instruction files: no edit.
|
||||
- System poster: no fact in `felhom-system-poster.facts.md` changed.
|
||||
- `unproven.py --summary`: NOT WALKED 35 of 55 — no number moved.
|
||||
- Commits: no Co-Authored-By line (the brief's rule).
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
|
||||
|
||||
**Updated 2026-10-16 (morning).** Storage identity build B shipped: agent 0.158.1 and golden 0.318.0 vouched by you, controller 0.318.0 is the floor (MinAgent 0.158.1), installer 1.33.0 published — new one-disk boxes get their own data area. Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
|
||||
**Updated 2026-10-16 (midday).** Controller 0.319.0 (the app page in panels, the login in one panel) is the floor, MinAgent 0.158.1 — `REPORT-app-page-tuning-2026-10-16.md`. **Earlier (morning):** Storage identity build B shipped: agent 0.158.1 and golden 0.318.0 vouched by you, controller 0.318.0 is the floor (MinAgent 0.158.1), installer 1.33.0 published — new one-disk boxes get their own data area. Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
|
||||
`documentation/backlog/STATUS-HISTORY.md`. Reports: `REPORT-night-2026-10-14.md`, `REPORT-immich-rollout-2026-10-14.md`, `REPORT-seo-felho-tarhely-2026-10-14.md`, `REPORT-immich-repair-2026-10-14.md`, `REPORT-power-cut-fixes-2026-10-14.md`, `REPORT-night-power-2026-10-13.md`, `REPORT-poweroff.md`.
|
||||
|
||||
## What works
|
||||
@@ -50,6 +50,7 @@
|
||||
| D9 | R-1062 | The N100's USB drive is also its whole-box backup drive. Split it in two anyway? | **No — keep one drive**; the lived-in box keeps its nightly whole-box backup | Split: the box loses its local whole-box backup (or add a second drive first) | it stays one drive; multi-drive tests stay on Tester 1 | — |
|
||||
|
||||
## Waiting on you — do
|
||||
- **Nézd meg a Calibre-Web oldalát a demo-hp-n** (`felhom.enkisfelhom.hu`, controller 0.319.0, minden dobozon kiadva). A fejléc, a fülsor és minden rész most külön panel (az „A” stílus). A belépési adatok egy panelben vannak, egymás mellett. A Calibre-Web jelszaváról a doboz nem tud, mert az alkalmazás előbb települt, mint hogy a sablonban jelszó lett volna — ezt most egy mondat mondja ki. Ha nem tetszik, írd meg, mit változtassak. Ha nem nézed meg: semmi nem romlik el.
|
||||
- **Discard two unclaimed boxes and delete Tester4** (hub, browser only): the build B scratch VMs registered as unclaimed boxes — registration ids 40 (`b43afa10…`) and 41 (`9c78ea30…`); both VMs are destroyed. Tester4 (the throwaway customer) is switched off the household way. If not: two dead entries stay in the Hosts list and Tester4 stays in Customers.
|
||||
- System poster needs a refresh: one-disk boxes now have a data area („Belső tárhely") on the system disk, outside the whole-guest backups (facts file updated 2026-10-16).
|
||||
- **Olvasd el a Plex és a Sonarr útmutatót a demo-hp-n** (az alkalmazás oldalán az „Útmutató” fül). Ha más formát szeretnél, egy menetben átírom mind a 62-t. A robot 2026-10-16-án bejelentkezve megnézte: mindkét oldal megjelenik magyarul és angolul, 4 rész és a GYIK, minden sor egyezik a katalógussal. Ha nem nézed meg: csak a forma marad emberi szemmel nem látott.
|
||||
|
||||
@@ -477,26 +477,50 @@ does not get to vouch for a version, so an overlay can neither set nor clear it.
|
||||
**[DESIGN] Guide text is DATA.** Plain text through `html/template` — no markdown, no raw HTML — so
|
||||
a step containing a tag is shown as the characters its author typed.
|
||||
|
||||
#### The app page's layout (v0.316.0, operator-approved mockup 2026-10-15)
|
||||
#### The app page's layout (v0.319.0, operator-approved 2026-10-16, section style A)
|
||||
|
||||
**[DESIGN]** Five rules; the first three are the mockup, the last two are what they imply.
|
||||
**[DESIGN]** The mockup canvas „Felhom app oldal — egy oszlop" (Settings screen), operator-approved
|
||||
2026-10-16 with section style **A — panels**. It replaces v0.316.0's hairline sections: with the hero,
|
||||
the tab bar and every section on the same page background, the operator found the page flat — nothing
|
||||
told one section from the next.
|
||||
|
||||
1. **One column of one width** (820px; the log tab alone is full width, because log lines are long).
|
||||
Boxes of differing widths read as a page assembled by accident, which is what v0.315.0 looked like.
|
||||
2. **A section is a hairline, not a card** — `padding: 28px 0` with a bottom `--line-soft`, no
|
||||
background, no border, no radius. A card says „this is a thing you act on", and most of an app
|
||||
page is a thing you READ; a card there is a promise the content cannot keep.
|
||||
3. **A box means something, and there are exactly two.** Blue (`--blue` on `--blue-dim`) when the app
|
||||
is WAITING for the household — an after-start value; amber (`--warn` on `--warn-dim`) when
|
||||
something is RISKY, such as a backup sharing a disk with the data it protects. The
|
||||
exception-colour principle, applied to shape as well as colour. Nothing informational is boxed.
|
||||
4. **The hero is not a box**: a 64px logo tile, the name, the tagline, hairline chips (state,
|
||||
lifecycle, update) and meta text (the address, the last automatic update). The tab bar is links
|
||||
with a 2px `--blue-bright` underline under the open one — no pills, no fills.
|
||||
5. **A success is not a banner.** The automatic-update SUCCESS line is meta text in the hero. It
|
||||
asked nothing of the household, so it is a date. Everything that DOES need a decision — a failed
|
||||
update, a hold, lost files, a missing drive, an interrupted install — stays a banner above the
|
||||
tabs, which is R-571's placement rule and is unchanged.
|
||||
Every panel is the column's full width — no narrower inner card. Boxes of differing widths read as
|
||||
a page assembled by accident.
|
||||
2. **The hero is a panel** (`--bg-1`, `--line` border, 20px 24px, 16px above it; the logo tile on
|
||||
`--bg-2`). Its content is unchanged: the name, the tagline, the chips, the meta text.
|
||||
3. **The tab bar is a panel** (`--bg-1`, 4px inside, 16px above, 20px below). A tab is 10px 18px,
|
||||
15px/600 in `--text-2`; the open tab is filled `--bg-2` with a 2px `--blue-bright` underline.
|
||||
4. **Every section is a panel, ONE style on every surface** — the four tabs and the install page,
|
||||
never mixed: `--bg-1`, `--line` border, `18px 24px 22px`, 16px apart; the title 17px/700 18px from
|
||||
the top edge with a `--line-soft` rule under it. The install page's form groups (`.form-section`)
|
||||
and its progress panel are drawn by the same rule. Pinned by `TestAppPage_OneSectionStyleEverywhere`.
|
||||
5. **Colour still means something.** A panel is neutral. Blue (`--blue` on `--blue-dim`) is the app
|
||||
WAITING for the household; amber (`--warn` on `--warn-dim`) is something RISKY. The callouts keep
|
||||
their colours, inside a panel or as their own.
|
||||
6. **A success is not a banner.** The automatic-update SUCCESS line is meta text in the hero.
|
||||
Everything that DOES need a decision — a failed update, a hold, lost files, a missing drive, an
|
||||
interrupted install — stays a banner above the tabs (R-571's placement rule, unchanged).
|
||||
|
||||
**[DESIGN] „Belépés az alkalmazásba" — the login in one panel.** The settings tab shows the app's
|
||||
login name and password side by side (stacked under 720px). A login field is a `type: password`
|
||||
field plus, when the template has one, the `secret` or `text` field with the same prefix and
|
||||
`USER`/`USERNAME`/`EMAIL` (`web/app_login.go`); a password with no clean pair stands alone. Values are
|
||||
fetched on demand (R-254, R-709) — the page learns only whether one is stored.
|
||||
|
||||
**[DESIGN] The empty-value rule.** A login field the box holds no value for is a SENTENCE („A doboz
|
||||
ennél a telepítésnél nem mentett jelszót. A belépéshez azt használd, amit az alkalmazásban
|
||||
beállítottál."), never an eye: the reveal endpoint answers 404 for it, a refusal the household could
|
||||
only meet by pressing. The page and the endpoint decide „stored" with ONE function
|
||||
(`storedValuePresent`). Measured cause of the case that earned it: an app installed before its
|
||||
template gained a `type: password` field — `InjectMissingFields` never fills one
|
||||
(`audits/app-page-tuning-2026-10-16/P0`).
|
||||
|
||||
**[DESIGN] „Cím és kulcsok" is rows, not the install form.** The settings tab does not re-render the
|
||||
install wizard's fields: the address is the „Cím" row, the storage is a „Tárhely: <név>" row, every
|
||||
other install answer and generated key is a row (keys keep their eye). The read-only hint sits under
|
||||
the title. `/apps/{slug}/install` opened for an installed app still renders the form read-only, with
|
||||
its secret guards.
|
||||
|
||||
**[DESIGN] The stylesheet's address changes with every build.** Every shell links
|
||||
`/static/style.css?v={{.AssetTag}}`, where the tag hashes version + `GitCommit` + `BuildTime` and
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Part 0 — why demo-hp's Calibre-Web has no stored password (2026-10-16, read only, no value printed)
|
||||
|
||||
Method: SSH to demo-hp (`ssh hp`) → `pct exec 9201`; the controller's own reveal endpoint
|
||||
(`POST /stacks/calibre-web/auto-field/reveal`, the one the eye button calls) through traefik inside the guest;
|
||||
Calibre-Web's own user table (`docker exec calibre-web sqlite3 /config/app.db 'select name from user'`), compared
|
||||
inside the guest. The hub's per-app page (`/apps/calibre-web`) with the CC token; then the box itself
|
||||
(demo-felhom, `ssh felhom-pve`) as the second channel.
|
||||
|
||||
## 1. Keys in calibre-web's app.yaml (9201), names only
|
||||
|
||||
```
|
||||
env: ADMIN_USER DOMAIN HDD_PATH SUBDOMAIN <- no ADMIN_PASSWORD key at all
|
||||
locked_fields: DOMAIN SUBDOMAIN HDD_PATH ADMIN_USER
|
||||
deployed_at: 2026-08-21T16:27:08Z
|
||||
default_login: changed_at 2026-09-29T07:21:06Z by household
|
||||
app.yaml mtime: 2026-10-01 11:26:10Z <- InjectMissingFields wrote ADMIN_USER here
|
||||
reveal ADMIN_PASSWORD -> 404, ok false, no value
|
||||
reveal ADMIN_USER -> 200, value present
|
||||
```
|
||||
|
||||
## 2. Install date vs the template
|
||||
|
||||
| what | when |
|
||||
|---|---|
|
||||
| calibre-web installed on 9201 | 2026-08-21 16:27Z |
|
||||
| `ADMIN_PASSWORD` (`type: password`) enters the template | catalog `d0e7e2e`, 2026-09-29 07:19Z |
|
||||
| `ADMIN_USER` (`type: secret`, `generate: hex:5`) enters the template | catalog `e9f50b5`, 2026-10-01 11:19Z |
|
||||
| the box injects `ADMIN_USER` into app.yaml | 2026-10-01 11:26:10Z (app.yaml mtime; A3 of `audits/calibre-name-and-prune-2026-10-01`) |
|
||||
| R-757 guard (no invented after_install-only secret) | controller `1991c74`, v0.298.0, 2026-10-06 |
|
||||
|
||||
`InjectMissingFields` (`internal/stacks/deploy.go`) fills only `secret`, `domain` and `subdomain`; a new
|
||||
`type: password` field falls to the `default:` branch (`requires manual configuration`) and is never stored. So the
|
||||
**hypothesis holds for the password**: installed before the field existed, never filled, and after_install (which sets
|
||||
it in the app) never ran for this install.
|
||||
|
||||
## 3. Is the stored ADMIN_USER invented?
|
||||
|
||||
```
|
||||
calibre users count 2 admin present False
|
||||
equal (stored ADMIN_USER is a Calibre-Web user): True
|
||||
```
|
||||
|
||||
**No — on demo-hp the name is NOT invented any more.** On 2026-10-01 11:27Z a session renamed Calibre-Web's admin
|
||||
user by hand to the name the box had just recorded (`audits/calibre-name-and-prune-2026-10-01/A/A2-demo-hp-rename.txt`,
|
||||
second block). The name the page shows is the real login name. **So the task's "remove the invented value from
|
||||
app.yaml on 9201" does not apply: removing it would hide a TRUE login name.** Nothing was removed.
|
||||
The password that goes with it was set by hand in the app; the operator holds it out-of-band.
|
||||
|
||||
## 4. Other boxes (hub, then the box)
|
||||
|
||||
The hub's `/apps/calibre-web` says „Deployments 2", affected customers demo-felhom and demo-hp (telemetry). Tester 1
|
||||
and Tester 2 are not listed. The second channel — the box: demo-felhom guest 9201 has `/opt/docker/stacks/calibre-web/`
|
||||
with only `.felhom.yml` + `docker-compose.yml` (catalog mirror), **no app.yaml, no calibre-web container** — it is not
|
||||
installed there now. **No customer box has this state. No register row for customer boxes.**
|
||||
|
||||
(The hub's „Deployments 2" counts telemetry history, not what is installed today — a presence-not-success reading;
|
||||
the box is the truth. NOT-A-FINDING for this task: the hub page labels it telemetry.)
|
||||
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 470 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,239 @@
|
||||
after login url: /launcher
|
||||
{
|
||||
"settings": {
|
||||
"path": "/apps/calibre-web/settings",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 3,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": true,
|
||||
"loginFields": [
|
||||
"ADMIN_USER",
|
||||
"ADMIN_PASSWORD"
|
||||
],
|
||||
"notStored": [
|
||||
"ADMIN_PASSWORD"
|
||||
],
|
||||
"reveals": [
|
||||
"ADMIN_USER"
|
||||
],
|
||||
"loginPairCols": "377px 377px",
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
},
|
||||
"data": {
|
||||
"path": "/apps/calibre-web/data",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 4,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
},
|
||||
"guide": {
|
||||
"path": "/apps/calibre-web/guide",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 10,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": [
|
||||
[
|
||||
"log-error",
|
||||
"Failed to load resource: the server responded with a status of 404 ()",
|
||||
"https://felhom.enkisfelhom.hu/static/assets/calibre-web-screenshot-3.webp"
|
||||
]
|
||||
]
|
||||
},
|
||||
"logs": {
|
||||
"path": "/apps/calibre-web/logs",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 1,
|
||||
"sectionWidths": [
|
||||
966
|
||||
],
|
||||
"columnWidth": 966,
|
||||
"sectionSpacing": [],
|
||||
"titleTopGap": null,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": 916,
|
||||
"console": []
|
||||
},
|
||||
"settings_390px": {
|
||||
"loginPairCols": "324px",
|
||||
"scrollWidth": 390
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
after login url: /launcher
|
||||
{
|
||||
"install": {
|
||||
"path": "/apps/grocy/install",
|
||||
"activeTab": null,
|
||||
"tabBar": null,
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 2,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": true,
|
||||
"deployForm": true,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,231 @@
|
||||
after login url: /launcher
|
||||
{
|
||||
"settings": {
|
||||
"path": "/apps/paperless-ngx/settings",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 3,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": true,
|
||||
"loginFields": [
|
||||
"PAPERLESS_ADMIN_USER",
|
||||
"PAPERLESS_ADMIN_PASSWORD"
|
||||
],
|
||||
"notStored": [],
|
||||
"reveals": [
|
||||
"PAPERLESS_ADMIN_PASSWORD"
|
||||
],
|
||||
"loginPairCols": "377px 377px",
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
},
|
||||
"data": {
|
||||
"path": "/apps/paperless-ngx/data",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 4,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
},
|
||||
"guide": {
|
||||
"path": "/apps/paperless-ngx/guide",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 10,
|
||||
"sectionWidths": [
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820,
|
||||
820
|
||||
],
|
||||
"columnWidth": 820,
|
||||
"sectionSpacing": [
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16
|
||||
],
|
||||
"titleTopGap": 19,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": null,
|
||||
"console": []
|
||||
},
|
||||
"logs": {
|
||||
"path": "/apps/paperless-ngx/logs",
|
||||
"activeTab": {
|
||||
"background-color": "rgb(22, 38, 63)",
|
||||
"color": "rgb(237, 242, 249)",
|
||||
"border-bottom-color": "rgb(46, 168, 245)",
|
||||
"border-bottom-width": "2px",
|
||||
"padding-top": "10px",
|
||||
"padding-left": "18px"
|
||||
},
|
||||
"tabBar": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "4px"
|
||||
},
|
||||
"hero": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"padding-top": "20px",
|
||||
"padding-left": "24px"
|
||||
},
|
||||
"section": {
|
||||
"background-color": "rgb(15, 27, 46)",
|
||||
"border-top-color": "rgb(34, 52, 79)",
|
||||
"border-top-width": "1px",
|
||||
"padding-top": "18px",
|
||||
"padding-left": "24px",
|
||||
"margin-bottom": "16px",
|
||||
"border-radius": "2px"
|
||||
},
|
||||
"visibleSections": 1,
|
||||
"sectionWidths": [
|
||||
966
|
||||
],
|
||||
"columnWidth": 966,
|
||||
"sectionSpacing": [],
|
||||
"titleTopGap": null,
|
||||
"loginPanel": false,
|
||||
"loginFields": [],
|
||||
"notStored": [],
|
||||
"reveals": [],
|
||||
"loginPairCols": null,
|
||||
"domainInput": false,
|
||||
"deployForm": false,
|
||||
"logBoxWidth": 916,
|
||||
"console": []
|
||||
},
|
||||
"settings_390px": {
|
||||
"loginPairCols": "324px",
|
||||
"scrollWidth": 390
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
## TestAppPage_LoginPairPanel — mutation on internal/web/app_login.go
|
||||
=== RUN TestAppPage_LoginPairPanel
|
||||
app_page_tuning_test.go:171: the login panel must hold BOTH halves (user=-1 password=301)
|
||||
--- FAIL: TestAppPage_LoginPairPanel (0.07s)
|
||||
## TestAppPage_NoStoredValueShowsSentence — mutation on internal/web/app_login.go
|
||||
=== RUN TestAppPage_NoStoredValueShowsSentence
|
||||
app_page_tuning_test.go:217: ADMIN_PASSWORD: no sentence for a field the box holds no value for
|
||||
app_page_tuning_test.go:220: ADMIN_PASSWORD: an eye is offered for a value the endpoint answers 404 for
|
||||
app_page_tuning_test.go:217: ADMIN_USER: no sentence for a field the box holds no value for
|
||||
app_page_tuning_test.go:220: ADMIN_USER: an eye is offered for a value the endpoint answers 404 for
|
||||
app_page_tuning_test.go:225: the page offers an eye for ADMIN_USER and the endpoint answers 404
|
||||
## TestAppPage_SettingsHasNoDuplicateDomain — mutation on internal/web/templates/app_settings.html
|
||||
=== RUN TestAppPage_SettingsHasNoDuplicateDomain
|
||||
app_page_tuning_test.go:242: the settings tab still renders id="auto-field-DOMAIN"
|
||||
app_page_tuning_test.go:242: the settings tab still renders id="deploy-form"
|
||||
app_page_tuning_test.go:242: the settings tab still renders id="field-HDD_PATH"
|
||||
--- FAIL: TestAppPage_SettingsHasNoDuplicateDomain (0.09s)
|
||||
## TestAppPage_OneSectionStyleEverywhere — mutation on internal/web/templates/app_logs.html
|
||||
=== RUN TestAppPage_OneSectionStyleEverywhere
|
||||
app_page_tuning_test.go:379: app_logs: a block outside the section style: <div data-tab-panel="logs">
|
||||
--- FAIL: TestAppPage_OneSectionStyleEverywhere (0.59s)
|
||||
## TestDeployPage_DeployedSelectShowsStoredHDDPath — row reads the DEFAULT drive's label
|
||||
## TestDeployPage_DeployedSelectShowsStoredHDDPath — row reads the DEFAULT drive's label
|
||||
networkstub_test.go:191: the storage row must name the STORED path's drive, got: data-app-storage-row="1">Tárhely (felhom-usb)
|
||||
networkstub_test.go:194: the storage row must NOT name the default drive for a deployed app, got: data-app-storage-row="1">Tárhely (felhom-usb)
|
||||
--- FAIL: TestDeployPage_DeployedSelectShowsStoredHDDPath (0.07s)
|
||||
@@ -0,0 +1,9 @@
|
||||
== tester-1
|
||||
unhealthy=0 restarting=0 exited=0 created=0
|
||||
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up 58 seconds (healthy)
|
||||
== demo-hp
|
||||
unhealthy=0 restarting=0 exited=0 created=0
|
||||
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up About a minute (healthy)
|
||||
== demo-felhom
|
||||
unhealthy=0 restarting=0 exited=0 created=0
|
||||
controller gitea.dooplex.hu/admin/felhom-controller:0.319.0 Up About a minute (healthy)
|
||||
@@ -0,0 +1,3 @@
|
||||
tester-1 Location: /customers/tester-1?flash=floor_set
|
||||
demo-hp Location: /customers/demo-hp?flash=floor_set
|
||||
demo-felhom Location: /customers/demo-felhom?flash=floor_set
|
||||
@@ -0,0 +1,3 @@
|
||||
felhom.enkisfelhom.hu 200
|
||||
felhom.enkicsifelhom.hu 200
|
||||
felhom.demo-felhom.eu 200
|
||||
@@ -0,0 +1,5 @@
|
||||
10:37:01
|
||||
HTTP/1.1 303 See Other
|
||||
Location: /configuration?flash=floor_set
|
||||
('min_controller_version', '0.319.0', '2026-10-16 10:37:02')
|
||||
('min_controller_version_declared_min_agent', '0.319.0=0.158.1', '2026-10-16 10:37:02')
|
||||
@@ -0,0 +1,3 @@
|
||||
10:35:57
|
||||
HTTP/1.1 303 See Other
|
||||
Location: /configuration?flash=floor_set
|
||||
@@ -0,0 +1,39 @@
|
||||
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
|
||||
== tester-1 (enkicsifelhom.hu): 13 public name(s)
|
||||
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://media.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://media.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://plex.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://plex.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://prowlarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://prowlarr.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://radarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://radarr.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://sonarr.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://sonarr.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://torrent.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://torrent.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://transmission.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://transmission.enkicsifelhom.hu/r1033-check?q=1
|
||||
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
|
||||
== demo-hp (enkisfelhom.hu): 13 public name(s)
|
||||
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://plex.enkisfelhom.hu/r1033-check?q=1 -> 301 https://plex.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
|
||||
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
|
||||
== demo-felhom (demo-felhom.eu): 8 public name(s)
|
||||
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://prowlarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://prowlarr.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://radarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://radarr.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://sonarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://sonarr.demo-felhom.eu/r1033-check?q=1
|
||||
OK http://torrent.demo-felhom.eu/r1033-check?q=1 -> 301 https://torrent.demo-felhom.eu/r1033-check?q=1
|
||||
== every public name redirects http -> https
|
||||
@@ -0,0 +1,4 @@
|
||||
['key', 'value', 'updated_at']
|
||||
('min_controller_version', '0.319.0', '2026-10-16 10:35:57')
|
||||
('artifact_min_agent', '0.155.0', '2026-10-16 07:23:10')
|
||||
('min_controller_version_declared_min_agent', '0.319.0=0.157.0', '2026-10-16 10:35:57')
|
||||
@@ -0,0 +1,6 @@
|
||||
('demo-felhom', '', '')
|
||||
('demo-hp', '', '')
|
||||
('tester-1', '', '')
|
||||
('Tester-2', '', '')
|
||||
('min_controller_version', '0.319.0', '2026-10-16 10:35:57')
|
||||
('min_controller_version_declared_min_agent', '0.319.0=0.157.0', '2026-10-16 10:35:57')
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each
|
||||
app-page tab, assert computed styles, collect console errors, save screenshots. The password is read
|
||||
from a 0600 file and only ever sent inside a CDP message; it is never printed."""
|
||||
import base64, json, os, socket, struct, subprocess, sys, time, urllib.request
|
||||
|
||||
CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell')
|
||||
BASE = sys.argv[1] # https://felhom.enkisfelhom.hu
|
||||
SLUG = sys.argv[2] # calibre-web
|
||||
PWFILE = sys.argv[3]
|
||||
OUT = sys.argv[4]
|
||||
PORT = 9333
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
prof = os.path.join(OUT, '.profile')
|
||||
proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}',
|
||||
'--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
for _ in range(50):
|
||||
try:
|
||||
tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json'))
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl']
|
||||
host, path = ws_url[len('ws://'):].split('/', 1)
|
||||
h, p = host.split(':')
|
||||
sock = socket.create_connection((h, int(p)))
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
|
||||
buf = b''
|
||||
while b'\r\n\r\n' not in buf:
|
||||
buf += sock.recv(4096)
|
||||
st = {'pending': buf.split(b'\r\n\r\n', 1)[1]}
|
||||
|
||||
def send(obj):
|
||||
data = json.dumps(obj).encode()
|
||||
hdr = bytearray([0x81])
|
||||
n = len(data)
|
||||
if n < 126: hdr.append(0x80 | n)
|
||||
elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n)
|
||||
else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n)
|
||||
mask = os.urandom(4); hdr += mask
|
||||
sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||
|
||||
def recv_exact(n):
|
||||
while len(st['pending']) < n:
|
||||
st['pending'] += sock.recv(1 << 20)
|
||||
out, st['pending'] = st['pending'][:n], st['pending'][n:]
|
||||
return out
|
||||
|
||||
def recv():
|
||||
msg = b''
|
||||
while True:
|
||||
b0, b1 = recv_exact(2)
|
||||
n = b1 & 0x7f
|
||||
if n == 126: n = struct.unpack('>H', recv_exact(2))[0]
|
||||
elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0]
|
||||
msg += recv_exact(n)
|
||||
if b0 & 0x80: return json.loads(msg)
|
||||
|
||||
console = []
|
||||
mid = [0]
|
||||
def call(method, **params):
|
||||
mid[0] += 1; i = mid[0]
|
||||
send({'id': i, 'method': method, 'params': params})
|
||||
while True:
|
||||
m = recv()
|
||||
if m.get('method') == 'Runtime.exceptionThrown':
|
||||
console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200]))
|
||||
elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'):
|
||||
console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200]))
|
||||
elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'):
|
||||
e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', '')))
|
||||
if m.get('id') == i:
|
||||
if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}')
|
||||
return m.get('result', {})
|
||||
|
||||
def ev(expr):
|
||||
r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True)
|
||||
return r.get('result', {}).get('value')
|
||||
|
||||
def goto(url):
|
||||
call('Page.navigate', url=url)
|
||||
for _ in range(100):
|
||||
time.sleep(0.2)
|
||||
if ev('document.readyState') == 'complete':
|
||||
break
|
||||
time.sleep(1.5) # images' onerror, layout
|
||||
|
||||
for d in ('Page', 'Runtime', 'Log', 'Network'):
|
||||
call(d + '.enable')
|
||||
pw = open(PWFILE).read().strip()
|
||||
goto(BASE + '/login')
|
||||
ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw))
|
||||
del pw
|
||||
time.sleep(3)
|
||||
print('after login url:', ev('location.pathname'))
|
||||
console.clear() # the login page is not under test
|
||||
|
||||
report = {}
|
||||
for tab in ['settings', 'data', 'guide', 'logs']:
|
||||
n0 = len(console)
|
||||
goto(f'{BASE}/apps/{SLUG}/{tab}')
|
||||
res = ev('''(function(){
|
||||
function cs(el, props){ if(!el) return null; var s=getComputedStyle(el), o={}; props.forEach(function(p){o[p]=s.getPropertyValue(p)}); return o; }
|
||||
var secs=[].slice.call(document.querySelectorAll('.app-section')).filter(function(e){return e.offsetParent!==null});
|
||||
var col=document.querySelector('.app-page');
|
||||
var t=document.querySelector('.app-section h3'); var first=secs[0];
|
||||
var gap = (t && first) ? Math.round(t.getBoundingClientRect().top - t.closest('.app-section').getBoundingClientRect().top) : null;
|
||||
var widths = secs.map(function(e){return Math.round(e.getBoundingClientRect().width)});
|
||||
var spacing = []; for (var i=1;i<secs.length;i++){ spacing.push(Math.round(secs[i].getBoundingClientRect().top - secs[i-1].getBoundingClientRect().bottom)); }
|
||||
return {path: location.pathname,
|
||||
activeTab: cs(document.querySelector('.app-tab-active'), ['background-color','color','border-bottom-color','border-bottom-width','padding-top','padding-left']),
|
||||
tabBar: cs(document.querySelector('.app-tabs'), ['background-color','border-top-color','padding-top']),
|
||||
hero: cs(document.querySelector('.app-hero'), ['background-color','border-top-color','padding-top','padding-left']),
|
||||
section: cs(first, ['background-color','border-top-color','border-top-width','padding-top','padding-left','margin-bottom','border-radius']),
|
||||
visibleSections: secs.length, sectionWidths: widths, columnWidth: col ? Math.round(col.getBoundingClientRect().width) : null,
|
||||
sectionSpacing: spacing, titleTopGap: gap,
|
||||
loginPanel: !!document.querySelector('[data-login-panel]'),
|
||||
loginFields: [].map.call(document.querySelectorAll('[data-login-field]'), function(e){return e.getAttribute('data-login-field')}),
|
||||
notStored: [].map.call(document.querySelectorAll('[data-login-not-stored]'), function(e){return e.getAttribute('data-login-not-stored')}),
|
||||
reveals: [].map.call(document.querySelectorAll('[data-login-reveal]'), function(e){return e.getAttribute('data-login-reveal')}),
|
||||
loginPairCols: (function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})(),
|
||||
domainInput: !!document.querySelector('#auto-field-DOMAIN, #field-DOMAIN'), deployForm: !!document.querySelector('#deploy-form'),
|
||||
logBoxWidth: (function(){var l=document.querySelector('.logs-container'); return l?Math.round(l.getBoundingClientRect().width):null})()
|
||||
};})()''')
|
||||
res['console'] = console[n0:]
|
||||
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
|
||||
open(os.path.join(OUT, f'{SLUG}-{tab}.png'), 'wb').write(base64.b64decode(shot['data']))
|
||||
report[tab] = res
|
||||
# phone width: the login pair stacks
|
||||
call('Emulation.setDeviceMetricsOverride', width=390, height=844, deviceScaleFactor=1, mobile=True)
|
||||
goto(f'{BASE}/apps/{SLUG}/settings')
|
||||
report['settings_390px'] = {'loginPairCols': ev("(function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})()"),
|
||||
'scrollWidth': ev('document.documentElement.scrollWidth')}
|
||||
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
|
||||
open(os.path.join(OUT, f'{SLUG}-settings-390px.png'), 'wb').write(base64.b64decode(shot['data']))
|
||||
print(json.dumps(report, indent=1, ensure_ascii=False))
|
||||
finally:
|
||||
proc.terminate()
|
||||
subprocess.run(['rm', '-rf', prof])
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each
|
||||
app-page tab, assert computed styles, collect console errors, save screenshots. The password is read
|
||||
from a 0600 file and only ever sent inside a CDP message; it is never printed."""
|
||||
import base64, json, os, socket, struct, subprocess, sys, time, urllib.request
|
||||
|
||||
CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell')
|
||||
BASE = sys.argv[1] # https://felhom.enkisfelhom.hu
|
||||
SLUG = sys.argv[2] # calibre-web
|
||||
PWFILE = sys.argv[3]
|
||||
OUT = sys.argv[4]
|
||||
PORT = 9333
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
prof = os.path.join(OUT, '.profile')
|
||||
proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}',
|
||||
'--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
for _ in range(50):
|
||||
try:
|
||||
tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json'))
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl']
|
||||
host, path = ws_url[len('ws://'):].split('/', 1)
|
||||
h, p = host.split(':')
|
||||
sock = socket.create_connection((h, int(p)))
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode())
|
||||
buf = b''
|
||||
while b'\r\n\r\n' not in buf:
|
||||
buf += sock.recv(4096)
|
||||
st = {'pending': buf.split(b'\r\n\r\n', 1)[1]}
|
||||
|
||||
def send(obj):
|
||||
data = json.dumps(obj).encode()
|
||||
hdr = bytearray([0x81])
|
||||
n = len(data)
|
||||
if n < 126: hdr.append(0x80 | n)
|
||||
elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n)
|
||||
else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n)
|
||||
mask = os.urandom(4); hdr += mask
|
||||
sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||
|
||||
def recv_exact(n):
|
||||
while len(st['pending']) < n:
|
||||
st['pending'] += sock.recv(1 << 20)
|
||||
out, st['pending'] = st['pending'][:n], st['pending'][n:]
|
||||
return out
|
||||
|
||||
def recv():
|
||||
msg = b''
|
||||
while True:
|
||||
b0, b1 = recv_exact(2)
|
||||
n = b1 & 0x7f
|
||||
if n == 126: n = struct.unpack('>H', recv_exact(2))[0]
|
||||
elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0]
|
||||
msg += recv_exact(n)
|
||||
if b0 & 0x80: return json.loads(msg)
|
||||
|
||||
console = []
|
||||
mid = [0]
|
||||
def call(method, **params):
|
||||
mid[0] += 1; i = mid[0]
|
||||
send({'id': i, 'method': method, 'params': params})
|
||||
while True:
|
||||
m = recv()
|
||||
if m.get('method') == 'Runtime.exceptionThrown':
|
||||
console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200]))
|
||||
elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'):
|
||||
console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200]))
|
||||
elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'):
|
||||
e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', '')))
|
||||
if m.get('id') == i:
|
||||
if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}')
|
||||
return m.get('result', {})
|
||||
|
||||
def ev(expr):
|
||||
r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True)
|
||||
return r.get('result', {}).get('value')
|
||||
|
||||
def goto(url):
|
||||
call('Page.navigate', url=url)
|
||||
for _ in range(100):
|
||||
time.sleep(0.2)
|
||||
if ev('document.readyState') == 'complete':
|
||||
break
|
||||
time.sleep(1.5) # images' onerror, layout
|
||||
|
||||
for d in ('Page', 'Runtime', 'Log', 'Network'):
|
||||
call(d + '.enable')
|
||||
pw = open(PWFILE).read().strip()
|
||||
goto(BASE + '/login')
|
||||
ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw))
|
||||
del pw
|
||||
time.sleep(3)
|
||||
print('after login url:', ev('location.pathname'))
|
||||
console.clear() # the login page is not under test
|
||||
|
||||
report = {}
|
||||
for tab in ['install']:
|
||||
n0 = len(console)
|
||||
goto(f'{BASE}/apps/{SLUG}/{tab}')
|
||||
res = ev('''(function(){
|
||||
function cs(el, props){ if(!el) return null; var s=getComputedStyle(el), o={}; props.forEach(function(p){o[p]=s.getPropertyValue(p)}); return o; }
|
||||
var secs=[].slice.call(document.querySelectorAll('.app-section, .deploy-form .form-section')).filter(function(e){return e.offsetParent!==null});
|
||||
var col=document.querySelector('.app-page');
|
||||
var t=document.querySelector('.deploy-form .form-section h4'); var first=secs[0];
|
||||
var gap = (t && first) ? Math.round(t.getBoundingClientRect().top - t.closest('.app-section, .form-section').getBoundingClientRect().top) : null;
|
||||
var widths = secs.map(function(e){return Math.round(e.getBoundingClientRect().width)});
|
||||
var spacing = []; for (var i=1;i<secs.length;i++){ spacing.push(Math.round(secs[i].getBoundingClientRect().top - secs[i-1].getBoundingClientRect().bottom)); }
|
||||
return {path: location.pathname,
|
||||
activeTab: cs(document.querySelector('.app-tab-active'), ['background-color','color','border-bottom-color','border-bottom-width','padding-top','padding-left']),
|
||||
tabBar: cs(document.querySelector('.app-tabs'), ['background-color','border-top-color','padding-top']),
|
||||
hero: cs(document.querySelector('.app-hero'), ['background-color','border-top-color','padding-top','padding-left']),
|
||||
section: cs(first, ['background-color','border-top-color','border-top-width','padding-top','padding-left','margin-bottom','border-radius']),
|
||||
visibleSections: secs.length, sectionWidths: widths, columnWidth: col ? Math.round(col.getBoundingClientRect().width) : null,
|
||||
sectionSpacing: spacing, titleTopGap: gap,
|
||||
loginPanel: !!document.querySelector('[data-login-panel]'),
|
||||
loginFields: [].map.call(document.querySelectorAll('[data-login-field]'), function(e){return e.getAttribute('data-login-field')}),
|
||||
notStored: [].map.call(document.querySelectorAll('[data-login-not-stored]'), function(e){return e.getAttribute('data-login-not-stored')}),
|
||||
reveals: [].map.call(document.querySelectorAll('[data-login-reveal]'), function(e){return e.getAttribute('data-login-reveal')}),
|
||||
loginPairCols: (function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})(),
|
||||
domainInput: !!document.querySelector('#auto-field-DOMAIN, #field-DOMAIN'), deployForm: !!document.querySelector('#deploy-form'),
|
||||
logBoxWidth: (function(){var l=document.querySelector('.logs-container'); return l?Math.round(l.getBoundingClientRect().width):null})()
|
||||
};})()''')
|
||||
res['console'] = console[n0:]
|
||||
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
|
||||
open(os.path.join(OUT, f'{SLUG}-{tab}.png'), 'wb').write(base64.b64decode(shot['data']))
|
||||
report[tab] = res
|
||||
print(json.dumps(report, indent=1, ensure_ascii=False)); raise SystemExit
|
||||
call('Emulation.setDeviceMetricsOverride', width=390, height=844, deviceScaleFactor=1, mobile=True)
|
||||
goto(f'{BASE}/apps/{SLUG}/settings')
|
||||
report['settings_390px'] = {'loginPairCols': ev("(function(){var g=document.querySelector('.app-login-pair'); return g?getComputedStyle(g).gridTemplateColumns:null})()"),
|
||||
'scrollWidth': ev('document.documentElement.scrollWidth')}
|
||||
shot = call('Page.captureScreenshot', format='png', captureBeyondViewport=True)
|
||||
open(os.path.join(OUT, f'{SLUG}-settings-390px.png'), 'wb').write(base64.b64decode(shot['data']))
|
||||
print(json.dumps(report, indent=1, ensure_ascii=False))
|
||||
finally:
|
||||
proc.terminate()
|
||||
subprocess.run(['rm', '-rf', prof])
|
||||
@@ -124,7 +124,7 @@ stopping line that lies.
|
||||
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
|
||||
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
|
||||
|
||||
## Apps & catalog — 20 rows (P2 2, P3 11, P4 7)
|
||||
## Apps & catalog — 21 rows (P2 2, P3 11, P4 8)
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -148,6 +148,7 @@ stopping line that lies.
|
||||
| **R-995** | Apps & catalog | P4 | **Removing an app through the product can leave unnamed Docker volumes and an empty import folder behind.** MEASURED 2026-10-13 on demo-felhom after 61 removals (`documentation/audits/sizing-2026-10-13/METHOD.md` „Teardown”, `evidence/leftover-volumes-after-bench.txt`): 8 unnamed volumes (4–72 KB; immich 1, romm 2, privatebin 3, onlyoffice 2 — volumes the images declare themselves) and `userdata/import/{paperless,calibre,grimmory}` (empty, created by the deploy). The removal answered 200 and its own leftover check was clean, because it looks only at labelled volumes. No data risk; disk and tidiness only. Removed by name on the bench. | **READY** — filed 2026-10-13 | — | Make the removal account for the containers' unnamed volumes (and an empty import folder it created), with a test that deploys an image that declares a VOLUME | CC |
|
||||
| **R-1067** | Apps & catalog | P4 | **The catalog still carries `pi_compatible:` on every template, and nothing reads it any more.** controller v0.315.0 removed the „Pi kompatibilis" badge and the „csak x86" tag from every page (Raspberry Pi is no longer supported, so one was noise and the other was a warning on almost every app). The `ResourceHints.PiCompatible` struct field and the catalog key were deliberately LEFT: a box still on 0.314.0 reads the key, and removing it would make that box show „csak x86" on every app. **Gated on the floor:** once the fleet floor is at or above 0.315.0, drop `pi_compatible:` from the templates and the field from `stacks.ResourceHints` in the same pair of commits. Harmless until then — an unread key costs nothing. | **BLOCKED** — filed 2026-10-15 | the fleet floor reaching 0.315.0 | Drop the catalog key and the struct field; grep the hub and the website for Pi wording at the same time (none found 2026-10-15) | CC |
|
||||
| **R-1043** | Apps & catalog | P4 | **Sonarr, Radarr and Prowlarr build their login redirect as an absolute `http://` URL.** They trust forwarded headers only from their „Trusted networks” (the household set `192.168.0.0/24`; traefik is `172.18.x`), so they ignore `X-Forwarded-Proto: https` and answer `Location: http://<name>/login` to an https visitor. Since controller v0.312.0 that hop is sent back to https by traefik (a GET, no password), so it is cosmetic; a template could seed traefik's network as trusted. (`audits/https-only-2026-10-14/``1A-3-4.txt`) | **OPEN** — filed 2026-10-14 | — | Seed the docker network as a trusted proxy in the *arr templates (with the R-1030 seed), measure the redirect | CC |
|
||||
| **R-1095** | Apps & catalog | P4 | **`/apps/{slug}/install` still opens for an app that is INSTALLED, and shows the install form read-only.** Found 2026-10-16 (app page tuning, controller v0.319.0): the settings tab stopped rendering the install form, but `appInstallPageHandler` renders `deploy` for any app, so a typed or stale `/install` address shows „<app> — Telepítés" with disabled fields and no button. Its secret guards hold (R-254/R-709 tests; parity cases `deploy_installed*`). | OPEN | the task forbade route changes | a 302 from `/apps/{slug}/install` to `/apps/{slug}/settings` for an installed app (the `redirectStackDeployGET` shape); then the deployed-only branches of `app_fields.html` can go | CC |
|
||||
|
||||
|
||||
## App updates — 4 rows (P3 4)
|
||||
|
||||