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
This commit is contained in:
admin committed 2026-10-16 12:41:30 +02:00
1 parent f1e1c8a60c
commit 59834feadf
35 files changed
+1032 -19

No files matched your search

@@ -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.)
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

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": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

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])