diff --git a/REPORT-app-page-tuning-2026-10-16.md b/REPORT-app-page-tuning-2026-10-16.md new file mode 100644 index 00000000..2fdda228 --- /dev/null +++ b/REPORT-app-page-tuning-2026-10-16.md @@ -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). diff --git a/STATUS.md b/STATUS.md index 20e09e91..89f12bee 100644 --- a/STATUS.md +++ b/STATUS.md @@ -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. diff --git a/documentation/architecture/02-controller-module-map.md b/documentation/architecture/02-controller-module-map.md index 2b953a7d..28f696bb 100644 --- a/documentation/architecture/02-controller-module-map.md +++ b/documentation/architecture/02-controller-module-map.md @@ -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: " 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 diff --git a/documentation/audits/app-page-tuning-2026-10-16/P0/README.md b/documentation/audits/app-page-tuning-2026-10-16/P0/README.md new file mode 100644 index 00000000..6a1cff62 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/P0/README.md @@ -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.) diff --git a/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-data.png b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-data.png new file mode 100644 index 00000000..aaaa0920 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-data.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-guide.png b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-guide.png new file mode 100644 index 00000000..0c7df907 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-guide.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-logs.png b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-logs.png new file mode 100644 index 00000000..6b5893ce Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-logs.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings-390px.png b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings-390px.png new file mode 100644 index 00000000..e3d6176d Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings-390px.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings.png b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings.png new file mode 100644 index 00000000..c919d751 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/before/calibre-web-settings.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-data.png b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-data.png new file mode 100644 index 00000000..aecf4660 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-data.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-guide.png b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-guide.png new file mode 100644 index 00000000..7a65fc5c Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-guide.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-logs.png b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-logs.png new file mode 100644 index 00000000..b96b9d15 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-logs.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings-390px.png b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings-390px.png new file mode 100644 index 00000000..afd3b820 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings-390px.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings.png b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings.png new file mode 100644 index 00000000..274caa26 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web-settings.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web.json b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web.json new file mode 100644 index 00000000..40d51599 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/browser/calibre-web.json @@ -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 + } +} diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.json b/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.json new file mode 100644 index 00000000..f3009b74 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.json @@ -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": [] + } +} diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.png b/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.png new file mode 100644 index 00000000..c4a9c1d5 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/grocy-install.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-data.png b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-data.png new file mode 100644 index 00000000..56f3ffb3 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-data.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-guide.png b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-guide.png new file mode 100644 index 00000000..6d1d7a7c Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-guide.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-logs.png b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-logs.png new file mode 100644 index 00000000..386c0781 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-logs.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings-390px.png b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings-390px.png new file mode 100644 index 00000000..00fc7ffd Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings-390px.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings.png b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings.png new file mode 100644 index 00000000..b27f17c7 Binary files /dev/null and b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx-settings.png differ diff --git a/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx.json b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx.json new file mode 100644 index 00000000..aa5c3695 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/browser/paperless-ngx.json @@ -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 + } +} diff --git a/documentation/audits/app-page-tuning-2026-10-16/redproof.txt b/documentation/audits/app-page-tuning-2026-10-16/redproof.txt new file mode 100644 index 00000000..bb2ced97 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/redproof.txt @@ -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:
+--- 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) diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/containers.txt b/documentation/audits/app-page-tuning-2026-10-16/release/containers.txt new file mode 100644 index 00000000..15d114bd --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/containers.txt @@ -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) diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/customer-floors-cleared.txt b/documentation/audits/app-page-tuning-2026-10-16/release/customer-floors-cleared.txt new file mode 100644 index 00000000..8600ce1c --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/customer-floors-cleared.txt @@ -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 diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/dashboards.txt b/documentation/audits/app-page-tuning-2026-10-16/release/dashboards.txt new file mode 100644 index 00000000..7635a913 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/dashboards.txt @@ -0,0 +1,3 @@ +felhom.enkisfelhom.hu 200 +felhom.enkicsifelhom.hu 200 +felhom.demo-felhom.eu 200 diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/global-floor-corrected.txt b/documentation/audits/app-page-tuning-2026-10-16/release/global-floor-corrected.txt new file mode 100644 index 00000000..a7f46822 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/global-floor-corrected.txt @@ -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') diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/global-floor.txt b/documentation/audits/app-page-tuning-2026-10-16/release/global-floor.txt new file mode 100644 index 00000000..148e3208 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/global-floor.txt @@ -0,0 +1,3 @@ +10:35:57 +HTTP/1.1 303 See Other +Location: /configuration?flash=floor_set diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/https-only.txt b/documentation/audits/app-page-tuning-2026-10-16/release/https-only.txt new file mode 100644 index 00000000..a0d104f3 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/https-only.txt @@ -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 diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floor-before-clear.txt b/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floor-before-clear.txt new file mode 100644 index 00000000..2ee6931b --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floor-before-clear.txt @@ -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') diff --git a/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floors-after.txt b/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floors-after.txt new file mode 100644 index 00000000..e9f1161e --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/release/hubdb-floors-after.txt @@ -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') diff --git a/documentation/audits/app-page-tuning-2026-10-16/tools/cdp_check.py b/documentation/audits/app-page-tuning-2026-10-16/tools/cdp_check.py new file mode 100644 index 00000000..7877f323 --- /dev/null +++ b/documentation/audits/app-page-tuning-2026-10-16/tools/cdp_check.py @@ -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;iH', 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/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 „ — 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)