R-415 fixed on hub main (no duplicate/nested/felhom.eu customer domain; R-138 option B); R-762 closed (9202 proven, catalog eec9a0d); R-905 opened (wger static in backups); decision sheet D10; 130 -> 129
gates / gates (push) Successful in 3m55s
gates / gates (push) Successful in 3m55s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
|
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
|
||||||
|
|
||||||
**Updated 2026-10-08 (afternoon): hub 0.143.1; demo-hp, demo-felhom and Tester 1 run agent 0.153.0 and controller
|
**Updated 2026-10-08 (afternoon): hub 0.143.1; demo-hp, demo-felhom and Tester 1 run agent 0.153.0 and controller
|
||||||
0.303.0 (nothing delivered today — tonight is the second kernel night). The open-items list is at 130. Reports:
|
0.303.0 (nothing delivered today — tonight is the second kernel night). The open-items list is at 129. Reports:
|
||||||
`REPORT-day-2026-10-08.md` (morning), `REPORT-day2-2026-10-08.md` (afternoon).**
|
`REPORT-day-2026-10-08.md` (morning), `REPORT-day2-2026-10-08.md` (afternoon).**
|
||||||
|
|
||||||
## Afternoon (2026-10-08): your four answers built, and one sheet of decisions
|
## Afternoon (2026-10-08): your four answers built, and one sheet of decisions
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
| D7 | Should the hub mail you an error when even one off-site snapshot disappears outside a clean-up window it opened? | Yes | ~½ session, hub | A deletion through any other key stays silent unless it removes over half of a household's history |
|
| D7 | Should the hub mail you an error when even one off-site snapshot disappears outside a clean-up window it opened? | Yes | ~½ session, hub | A deletion through any other key stays silent unless it removes over half of a household's history |
|
||||||
| D8 | After a failed off-site restore of one app: keep the app stopped for support now, and „put back exactly as it was" next? | Yes, both, in that order | ~½ session now; ~1 session + a test + disk space for one copy later | The app restarts on a mix of old files and a newer database, and the screen says all is back as it was |
|
| D8 | After a failed off-site restore of one app: keep the app stopped for support now, and „put back exactly as it was" next? | Yes, both, in that order | ~½ session now; ~1 session + a test + disk space for one copy later | The app restarts on a mix of old files and a newer database, and the screen says all is back as it was |
|
||||||
| D9 | May CC install the DooPlex job that removes a deleted customer's copy (dry run first, then daily)? | Yes, after tomorrow's releases are read back | ~30 min; one new local token on DooPlex | A deleted customer's copy stays on DooPlex, and the privacy-notice line „within 30 days" is not true |
|
| D9 | May CC install the DooPlex job that removes a deleted customer's copy (dry run first, then daily)? | Yes, after tomorrow's releases are read back | ~30 min; one new local token on DooPlex | A deleted customer's copy stays on DooPlex, and the privacy-notice line „within 30 days" is not true |
|
||||||
|
| D10 | wger's install check still assumes 100 MB; measured 250 MB with its proper server. Raise it to 256 MB? | Yes — only boxes with room can install it | One line in the catalog; a box short of memory can no longer install wger | The install check lets wger onto a box that cannot hold it (wger is hidden today, so no one meets this yet) |
|
||||||
|
|
||||||
Full designs: `documentation/audits/day-2026-10-08/`.
|
Full designs: `documentation/audits/day-2026-10-08/`.
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ Full designs: `documentation/audits/day-2026-10-08/`.
|
|||||||
in the menu goes to the same page in the other language.
|
in the menu goes to the same page in the other language.
|
||||||
- **A missing-page (404) page exists now.**
|
- **A missing-page (404) page exists now.**
|
||||||
- **The language link is now a globe icon**, like the dashboard's: a click shows „Magyar" and „English".
|
- **The language link is now a globe icon**, like the dashboard's: a click shows „Magyar" and „English".
|
||||||
|
- **The contact form's lost program code was searched for everywhere I can reach: not found.** The running program is now saved in git, so it cannot be lost. A plan for a replacement is written. One question for you: is the February folder on your Windows computer?
|
||||||
|
|
||||||
**Needs you:** two choices in `REPORT-website-refresh.md`. If nothing: SparkyFitness stays listed; the contact
|
**Needs you:** two choices in `REPORT-website-refresh.md`. If nothing: SparkyFitness stays listed; the contact
|
||||||
mailer's source stays lost.
|
mailer's source stays lost.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# R-762 on scratch 9202 (2026-10-08, 09:50–10:09 CEST)
|
||||||
|
|
||||||
|
wger was installed fresh from the drill catalog (commit `bebbac8`: wger un-hidden plus the gunicorn definition, DRILL
|
||||||
|
only; reverted as `95876a3`). The install went through the product's deploy endpoint (`tools/box9202.py install`).
|
||||||
|
`tools/repoint.py` moved 9202's catalog setting to the drill catalog and back.
|
||||||
|
|
||||||
|
| check | result |
|
||||||
|
|---|---|
|
||||||
|
| workers | 2 × „Booting worker" (`wger.log`); env `WGER_USE_GUNICORN=True`, `WEB_CONCURRENCY=2` |
|
||||||
|
| login page | 200 |
|
||||||
|
| CSS (the page's 3 links, through wger-files) | 200 / 200 / 200 (2481 B, 277042 B, 1006 B, `text/css`) |
|
||||||
|
| photo | POST `/api/v2/gallery/` 201; read back 200, 179 B, `image/png` |
|
||||||
|
| control | an unknown `/static/` file 404 |
|
||||||
|
| seed | a weight entry 201, read back 200 |
|
||||||
|
| 600 s watch (10,944 requests: 8,208 × 200, 2,736 × 302) | anon peak 258,273,280 B = 246.3 MiB = 64.1 % of 384M (memory.stat `anon`, every 2 s); oom 0, oom_kill 0; restarts 0, oomkilled false, healthy (wger and wger-files) |
|
||||||
|
| after the watch (`plateau-check.txt`) | anon 246 MiB for 1,200 more login-page loads; oom_kill 0; restarts 0 |
|
||||||
|
|
||||||
|
memory.peak reached the limit (402,657,280 B) because it counts the page cache (`after-watch-memory.txt`: file 22 MiB,
|
||||||
|
kernel 49 MiB). The anon figure is what counts.
|
||||||
|
|
||||||
|
The bench figure was 170 MiB (44 %). The bench load was 302 redirects only. Here the load also rendered the login page
|
||||||
|
and served a photo upload. The cause of the difference was not measured.
|
||||||
|
|
||||||
|
**Removal through the product** (`run.txt`): stop 200, remove (with data) 200, volumes `wger_wger_data`,
|
||||||
|
`wger_wger_media` and `wger_wger_static` removed. Afterwards: no wger container, no wger volume, `deployed=False`.
|
||||||
|
`/opt/docker/stacks/wger` holds only the catalog's synced `.felhom.yml` and `docker-compose.yml`. It held the same two
|
||||||
|
files before the test, because every catalog app has this directory.
|
||||||
|
|
||||||
|
**Put back:** `repo_url` = `https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git`. `controller.yaml` sha256
|
||||||
|
`7739ad7b…` is the same before and after. The save copy was deleted. Deployed apps are paperless-ngx and privatebin,
|
||||||
|
as before.
|
||||||
|
|
||||||
|
The generated admin password was never written. The image's default password in `wger.log` is redacted.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
anon 258273280
|
||||||
|
file 23224320
|
||||||
|
kernel 51847168
|
||||||
|
shmem 0
|
||||||
|
peak=402657280
|
||||||
|
wger-files 7.48MiB / 32MiB
|
||||||
|
wger 295.8MiB / 384MiB
|
||||||
|
PID RSS COMMAND
|
||||||
|
1 3804 /bin/bash /home/wger/entrypoint.sh
|
||||||
|
58 132060 /usr/bin/python3 /home/wger/.local/bin/gunicorn wger.wsgi:application --preload --bind 0.0.0.0:8000
|
||||||
|
59 127124 /usr/bin/python3 /home/wger/.local/bin/gunicorn wger.wsgi:application --preload --bind 0.0.0.0:8000
|
||||||
|
60 121560 /usr/bin/python3 /home/wger/.local/bin/gunicorn wger.wsgi:application --preload --bind 0.0.0.0:8000
|
||||||
|
464 4316 ps -o pid,rss,args
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"app": "wger",
|
||||||
|
"venue": "scratch guest 9202 on demo-hp, drill catalog, the product's deploy endpoint",
|
||||||
|
"drill_commit": "bebbac8 DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)",
|
||||||
|
"pinned": {
|
||||||
|
"wger": "wger/server:2.7",
|
||||||
|
"wger-files": "nginx:1.30.5-alpine"
|
||||||
|
},
|
||||||
|
"seed_read": true,
|
||||||
|
"login_page": "200",
|
||||||
|
"css": {
|
||||||
|
"/static/css/workout-manager.7007d84ce531.css": "200 2481 text/css",
|
||||||
|
"/static/bootstrap-compiled.80a6279921f8.css": "200 277042 text/css",
|
||||||
|
"/static/css/bootstrap-custom.400ad578123c.css": "200 1006 text/css"
|
||||||
|
},
|
||||||
|
"photo_post": "201",
|
||||||
|
"photo_bytes_posted": 179,
|
||||||
|
"photo_path": "/media/gallery/1/f3391508-f895-4fc4-9688-7a42dc158932.png",
|
||||||
|
"photo_read": "200 179 image/png",
|
||||||
|
"control_unknown_static": "404 153 text/html",
|
||||||
|
"container": "cgroup=/sys/fs/cgroup/system.slice/docker-42589be792575b34221f0ff8ab0141f576e21db37a1ce1a4214e7e3385e1da46.scope\nWGER_USE_GUNICORN=True\nWEB_CONCURRENCY=2\nUsing gunicorn on port 8000...\n[2026-10-08 09:54:38 +0200] [58] [INFO] Starting gunicorn 26.1.0\n[2026-10-08 09:54:38 +0200] [58] [INFO] Listening at: http://0.0.0.0:8000 (58)\n[2026-10-08 09:54:38 +0200] [59] [INFO] Booting worker with pid: 59\n[2026-10-08 09:54:38 +0200] [60] [INFO] Booting worker with pid: 60\n402653184\n",
|
||||||
|
"watch_s": 600.1,
|
||||||
|
"watch_requests": 10944,
|
||||||
|
"watch_codes": {
|
||||||
|
"200": 8208,
|
||||||
|
"302": 2736
|
||||||
|
},
|
||||||
|
"watch": "anon_max=258273280\nmemory.max=402653184\noom 0\noom_kill 0\nrestarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.480653193Z\nrestarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.678387772Z\n2\n",
|
||||||
|
"anon_max_bytes": 258273280,
|
||||||
|
"anon_max_mib": 246.3,
|
||||||
|
"anon_pct_of_384M": 64.1
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
08:06:00 round=1 gets=100 anon_MiB=246
|
||||||
|
08:06:05 round=2 gets=100 anon_MiB=246
|
||||||
|
08:06:10 round=3 gets=100 anon_MiB=246
|
||||||
|
08:06:14 round=4 gets=100 anon_MiB=246
|
||||||
|
08:06:19 round=5 gets=100 anon_MiB=246
|
||||||
|
08:06:23 round=6 gets=100 anon_MiB=246
|
||||||
|
08:06:28 round=7 gets=100 anon_MiB=246
|
||||||
|
08:06:32 round=8 gets=100 anon_MiB=246
|
||||||
|
08:06:37 round=9 gets=100 anon_MiB=246
|
||||||
|
08:06:41 round=10 gets=100 anon_MiB=246
|
||||||
|
08:06:46 round=11 gets=100 anon_MiB=246
|
||||||
|
08:06:50 round=12 gets=100 anon_MiB=246
|
||||||
|
oom_kill 0
|
||||||
|
restarts=0
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
09:50:14 drill: bebbac8 DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)
|
||||||
|
09:52:49 synced template on the box: 2 18:lifecycle: available
|
||||||
|
09:54:56 wger: POST /api/v2/weightentry/ http=201
|
||||||
|
09:54:56 wger: readback of the seeded weight entry http=200 found=True
|
||||||
|
09:54:56 login page -> 200; CSS {'/static/css/workout-manager.7007d84ce531.css': '200 2481 text/css', '/static/bootstrap-compiled.80a6279921f8.css': '200 277042 text/css', '/static/css/bootstrap-custom.400ad578123c.css': '200 1006 text/css'}
|
||||||
|
09:54:57 POST /api/v2/gallery/ (179 B PNG) -> 201; read back /media/gallery/1/f3391508-f895-4fc4-9688-7a42dc158932.png -> 200 179 image/png; control -> 404 153 text/html
|
||||||
|
09:55:00 container:
|
||||||
|
cgroup=/sys/fs/cgroup/system.slice/docker-42589be792575b34221f0ff8ab0141f576e21db37a1ce1a4214e7e3385e1da46.scope
|
||||||
|
WGER_USE_GUNICORN=True
|
||||||
|
WEB_CONCURRENCY=2
|
||||||
|
Using gunicorn on port 8000...
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Starting gunicorn 26.1.0
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Listening at: http://0.0.0.0:8000 (58)
|
||||||
|
[2026-10-08 09:54:38 +0200] [59] [INFO] Booting worker with pid: 59
|
||||||
|
[2026-10-08 09:54:38 +0200] [60] [INFO] Booting worker with pid: 60
|
||||||
|
402653184
|
||||||
|
|
||||||
|
10:05:10 watch:
|
||||||
|
anon_max=258273280
|
||||||
|
memory.max=402653184
|
||||||
|
oom 0
|
||||||
|
oom_kill 0
|
||||||
|
restarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.480653193Z
|
||||||
|
restarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.678387772Z
|
||||||
|
2
|
||||||
|
|
||||||
|
10:05:13 RECORD {"app": "wger", "venue": "scratch guest 9202 on demo-hp, drill catalog, the product's deploy endpoint", "drill_commit": "bebbac8 DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)", "pinned": {"wger": "wger/server:2.7", "wger-files": "nginx:1.30.5-alpine"}, "seed_read": true, "login_page": "200", "css": {"/static/css/workout-manager.7007d84ce531.css": "200 2481 text/css", "/static/bootstrap-compiled.80a6279921f8.css": "200 277042 text/css", "/static/css/bootstrap-custom.400ad578123c.css": "200 1006 text/css"}, "photo_post": "201", "photo_bytes_posted": 179, "photo_path": "/media/gallery/1/f3391508-f895-4fc4-9688-7a42dc158932.png", "photo_read": "200 179 image/png", "control_unknown_static": "404 153 text/html", "watch_s": 600.1, "watch_requests": 10944, "watch_codes": {"200": 8208, "302": 2736}, "anon_max_bytes": 258273280, "anon_max_mib": 246.3, "anon_pct_of_384M": 64.1}
|
||||||
|
10:07:50 remove -> 200
|
||||||
|
10:07:53 after remove:
|
||||||
|
vols:
|
||||||
|
stackdir:
|
||||||
|
total 28
|
||||||
|
drwxr-xr-x 2 root root 4096 Oct 8 08:07 .
|
||||||
|
drwxr-xr-x 65 root root 4096 Oct 2 05:49 ..
|
||||||
|
-rw-r--r-- 1 root root 7619 Oct 8 07:50 .felhom.yml
|
||||||
|
-rw-r--r-- 1 root root 8672 Oct 8 07:50 docker-compose.yml
|
||||||
|
end
|
||||||
|
|
||||||
|
10:07:53 stack: deployed=False state=not_deployed
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
09:50:14 drill: bebbac8 DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)
|
||||||
|
09:52:49 synced template on the box: 2 18:lifecycle: available
|
||||||
|
09:52:49 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['ADMIN_PASSWORD']
|
||||||
|
09:52:50 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
|
||||||
|
09:54:55 [1] deployed, controller state=running, pinned={'wger': 'wger/server:2.7', 'wger-files': 'nginx:1.30.5-alpine'}
|
||||||
|
09:54:56 wger: POST /api/v2/weightentry/ http=201
|
||||||
|
09:54:56 wger: readback of the seeded weight entry http=200 found=True
|
||||||
|
09:54:56 login page -> 200; CSS {'/static/css/workout-manager.7007d84ce531.css': '200 2481 text/css', '/static/bootstrap-compiled.80a6279921f8.css': '200 277042 text/css', '/static/css/bootstrap-custom.400ad578123c.css': '200 1006 text/css'}
|
||||||
|
09:54:57 POST /api/v2/gallery/ (179 B PNG) -> 201; read back /media/gallery/1/f3391508-f895-4fc4-9688-7a42dc158932.png -> 200 179 image/png; control -> 404 153 text/html
|
||||||
|
09:55:00 container:
|
||||||
|
cgroup=/sys/fs/cgroup/system.slice/docker-42589be792575b34221f0ff8ab0141f576e21db37a1ce1a4214e7e3385e1da46.scope
|
||||||
|
WGER_USE_GUNICORN=True
|
||||||
|
WEB_CONCURRENCY=2
|
||||||
|
Using gunicorn on port 8000...
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Starting gunicorn 26.1.0
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Listening at: http://0.0.0.0:8000 (58)
|
||||||
|
[2026-10-08 09:54:38 +0200] [59] [INFO] Booting worker with pid: 59
|
||||||
|
[2026-10-08 09:54:38 +0200] [60] [INFO] Booting worker with pid: 60
|
||||||
|
402653184
|
||||||
|
|
||||||
|
10:05:10 watch:
|
||||||
|
anon_max=258273280
|
||||||
|
memory.max=402653184
|
||||||
|
oom 0
|
||||||
|
oom_kill 0
|
||||||
|
restarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.480653193Z
|
||||||
|
restarts=0 oomkilled=false status=running health=healthy started=2026-10-08T07:53:22.678387772Z
|
||||||
|
2
|
||||||
|
|
||||||
|
10:05:13 RECORD {"app": "wger", "venue": "scratch guest 9202 on demo-hp, drill catalog, the product's deploy endpoint", "drill_commit": "bebbac8 DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)", "pinned": {"wger": "wger/server:2.7", "wger-files": "nginx:1.30.5-alpine"}, "seed_read": true, "login_page": "200", "css": {"/static/css/workout-manager.7007d84ce531.css": "200 2481 text/css", "/static/bootstrap-compiled.80a6279921f8.css": "200 277042 text/css", "/static/css/bootstrap-custom.400ad578123c.css": "200 1006 text/css"}, "photo_post": "201", "photo_bytes_posted": 179, "photo_path": "/media/gallery/1/f3391508-f895-4fc4-9688-7a42dc158932.png", "photo_read": "200 179 image/png", "control_unknown_static": "404 153 text/html", "watch_s": 600.1, "watch_requests": 10944, "watch_codes": {"200": 8208, "302": 2736}, "anon_max_bytes": 258273280, "anon_max_mib": 246.3, "anon_pct_of_384M": 64.1}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""box9202.py — R-762 9202 half (2026-10-08 afternoon). ONE process, through the product.
|
||||||
|
phase install: drill commit (wger un-hidden + the gunicorn definition, DRILL only), sync, deploy, seed, login page,
|
||||||
|
CSS, photo, workers, env; then a ~10-minute watch (anon from memory.stat, oom_kill, restarts) with light load.
|
||||||
|
The removal is a separate phase (remove) so a failure leaves something to look at.
|
||||||
|
Evidence -> $EVD (no secrets: the generated admin password is never written)."""
|
||||||
|
import json, os, re, struct, subprocess, sys, tempfile, time, zlib, secrets
|
||||||
|
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||||
|
import box_walk as w
|
||||||
|
import upgrade_fixtures_box as fixtures
|
||||||
|
|
||||||
|
APP, SUB = "wger", "fitness"
|
||||||
|
EVD = os.environ["EVD"]; os.makedirs(EVD, exist_ok=True)
|
||||||
|
log = open(f"{EVD}/run.txt", "a", buffering=1)
|
||||||
|
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
|
||||||
|
NEWC = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/wger/docker-compose.yml"
|
||||||
|
REC = {"app": APP, "venue": "scratch guest 9202 on demo-hp, drill catalog, the product's deploy endpoint"}
|
||||||
|
|
||||||
|
|
||||||
|
def say(*a):
|
||||||
|
w.say(*a); log.write(time.strftime("%H:%M:%S ") + " ".join(map(str, a)) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def git(*a):
|
||||||
|
return subprocess.run(["git", "-C", D, *a], check=True, capture_output=True, text=True).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def png(path, n=64):
|
||||||
|
rgb = secrets.token_bytes(3)
|
||||||
|
raw = b"".join(b"\x00" + rgb * n for _ in range(n))
|
||||||
|
def chunk(t, d):
|
||||||
|
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff)
|
||||||
|
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", n, n, 8, 2, 0, 0, 0))
|
||||||
|
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
|
||||||
|
open(path, "wb").write(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(path):
|
||||||
|
"""code, bytes, content-type of one GET through the household's route (traefik, gate cookie). Body discarded."""
|
||||||
|
gc = w.GATE.get(SUB) or w.gate_cookie(SUB)
|
||||||
|
args = ["curl", "-sSk", "--max-time", "45", "-H", f"Host: {SUB}.{w.DOMAIN}", "-o", "/dev/null",
|
||||||
|
"-w", "%{http_code} %{size_download} %{content_type}"]
|
||||||
|
if gc:
|
||||||
|
args += ["-H", f"Cookie: {gc}"]
|
||||||
|
r = w.sh(args + [f"{w.BASE}{path}"], timeout=90)
|
||||||
|
return (r.stdout or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def css_links():
|
||||||
|
rc, code, page = w.app_curl(SUB, "/en/user/login")
|
||||||
|
links = re.findall(r'(/static/[^"]+\.css)', page or "")[:3]
|
||||||
|
return code, {l: fetch(l) for l in links}
|
||||||
|
|
||||||
|
|
||||||
|
CG = 'ID=$(docker inspect -f "{{.Id}}" wger); CG=$(find /sys/fs/cgroup -maxdepth 6 -type d -name "*$ID*" | head -1)'
|
||||||
|
|
||||||
|
phase = sys.argv[1]
|
||||||
|
w.login()
|
||||||
|
if phase == "install":
|
||||||
|
if w.stack(APP).get("deployed"):
|
||||||
|
sys.exit(say("STOP: wger already deployed on 9202 — not this run's") or 1)
|
||||||
|
git("pull", "-q", "--rebase", "origin", "main")
|
||||||
|
fy = f"{D}/templates/{APP}/.felhom.yml"
|
||||||
|
s = open(fy).read()
|
||||||
|
open(fy, "w").write(s.replace("lifecycle: hidden", "lifecycle: available", 1))
|
||||||
|
open(f"{D}/templates/{APP}/docker-compose.yml", "w").write(open(NEWC).read())
|
||||||
|
git("add", f"templates/{APP}/.felhom.yml", f"templates/{APP}/docker-compose.yml")
|
||||||
|
git("commit", "-q", "-m", "DRILL wger: un-hidden + the R-762 gunicorn definition (DRILL only, 2026-10-08)")
|
||||||
|
git("push", "-q", "origin", "main")
|
||||||
|
REC["drill_commit"] = git("log", "--oneline", "-1").strip()
|
||||||
|
say("drill:", REC["drill_commit"])
|
||||||
|
for _ in range(12):
|
||||||
|
w.sync_rescan(); time.sleep(5)
|
||||||
|
stk = w.guest(f"grep -c WGER_USE_GUNICORN /opt/docker/stacks/{APP}/docker-compose.yml; grep -n lifecycle /opt/docker/stacks/{APP}/.felhom.yml").split()
|
||||||
|
if stk and stk[0] == "1" and "available" in " ".join(stk):
|
||||||
|
break
|
||||||
|
say("synced template on the box:", " ".join(stk))
|
||||||
|
if not w.deploy(APP, SUB):
|
||||||
|
sys.exit(say("RESULT the install did not complete") or 1)
|
||||||
|
REC["pinned"] = (w.stack(APP).get("app_config") or {}).get("pinned_images")
|
||||||
|
fx = fixtures.FIXTURES[APP]
|
||||||
|
tok = fx.seed(w, SUB, say)
|
||||||
|
if tok is None:
|
||||||
|
sys.exit(say(f"RESULT seed failed: {getattr(fx, 'tried', '')}") or 1)
|
||||||
|
REC["seed_read"] = fx.verify(w, SUB, tok, say)
|
||||||
|
REC["login_page"], REC["css"] = css_links()
|
||||||
|
say(f"login page -> {REC['login_page']}; CSS {REC['css']}")
|
||||||
|
jar = tempfile.mktemp(prefix="wger-jar-")
|
||||||
|
hdr, why = fx._login(w, SUB, tok["pw"], jar)
|
||||||
|
pic = tempfile.mktemp(prefix="wger-photo-", suffix=".png")
|
||||||
|
size = png(pic)
|
||||||
|
rc, code, out = w.app_curl(SUB, "/api/v2/gallery/", *hdr, "-F", f"image=@{pic};type=image/png", "-F",
|
||||||
|
"date=2026-10-08", "-F", "description=r762", method="POST")
|
||||||
|
img = None
|
||||||
|
try:
|
||||||
|
img = json.loads(out).get("image")
|
||||||
|
except Exception:
|
||||||
|
say(f" gallery body {out[:200]}")
|
||||||
|
path = re.sub(r"^https?://[^/]+", "", img or "")
|
||||||
|
REC["photo_post"] = code; REC["photo_bytes_posted"] = size; REC["photo_path"] = path
|
||||||
|
REC["photo_read"] = fetch(path) if path else None
|
||||||
|
REC["control_unknown_static"] = fetch("/static/r762-nonexistent.css")
|
||||||
|
say(f"POST /api/v2/gallery/ ({size} B PNG) -> {code}; read back {path} -> {REC['photo_read']}; control -> {REC['control_unknown_static']}")
|
||||||
|
for f in (jar, pic):
|
||||||
|
if os.path.exists(f):
|
||||||
|
os.unlink(f)
|
||||||
|
g = w.guest(CG + '''
|
||||||
|
echo "cgroup=$CG"
|
||||||
|
docker exec wger sh -c 'env | grep -E "^(WGER_USE_GUNICORN|WEB_CONCURRENCY)="'
|
||||||
|
docker logs wger 2>&1 | grep -E "Using gunicorn|Using django|Booting worker|Listening at|Starting gunicorn|Starting development server"
|
||||||
|
cat $CG/memory.max''')
|
||||||
|
say("container:\n" + g)
|
||||||
|
REC["container"] = g
|
||||||
|
# the watch: ~10 min, anon sampled every 2 s in the guest; light load from here (login page + CSS + photo)
|
||||||
|
w.guest(CG + '''
|
||||||
|
rm -f /root/r762w.*; touch /root/r762w.on
|
||||||
|
nohup sh -c 'max=0; while [ -f /root/r762w.on ]; do a=$(awk "\\$1==\\"anon\\"{print \\$2}" '"$CG"'/memory.stat 2>/dev/null); [ -n "$a" ] && [ "$a" -gt "$max" ] && max=$a && echo $max > /root/r762w.max; sleep 2; done' >/dev/null 2>&1 &
|
||||||
|
echo started''')
|
||||||
|
t0 = time.time(); n = 0; codes = {}
|
||||||
|
while time.time() - t0 < 600:
|
||||||
|
for p in ("/en/user/login", list(REC["css"])[0] if REC["css"] else "/en/user/login", path or "/en/user/login"):
|
||||||
|
c = fetch(p).split(" ")[0]; codes[c] = codes.get(c, 0) + 1; n += 1
|
||||||
|
rc, c, _ = w.app_curl(SUB, "/en/dashboard"); codes[c] = codes.get(c, 0) + 1; n += 1
|
||||||
|
REC["watch_s"] = round(time.time() - t0, 1); REC["watch_requests"] = n; REC["watch_codes"] = codes
|
||||||
|
g = w.guest(CG + '''
|
||||||
|
rm -f /root/r762w.on; sleep 3
|
||||||
|
echo "anon_max=$(cat /root/r762w.max)"
|
||||||
|
echo "memory.max=$(cat $CG/memory.max)"
|
||||||
|
grep -E "^(oom|oom_kill) " $CG/memory.events
|
||||||
|
docker inspect -f "restarts={{.RestartCount}} oomkilled={{.State.OOMKilled}} status={{.State.Status}} health={{.State.Health.Status}} started={{.State.StartedAt}}" wger wger-files
|
||||||
|
docker logs wger 2>&1 | grep -c "Booting worker"
|
||||||
|
rm -f /root/r762w.*''')
|
||||||
|
say("watch:\n" + g)
|
||||||
|
REC["watch"] = g
|
||||||
|
m = re.search(r"anon_max=(\d+)", g)
|
||||||
|
if m:
|
||||||
|
a = int(m.group(1)); REC["anon_max_bytes"] = a
|
||||||
|
REC["anon_max_mib"] = round(a / 1048576, 1); REC["anon_pct_of_384M"] = round(100 * a / (384 * 1048576), 1)
|
||||||
|
logs = w.guest("docker logs wger 2>&1 | tail -n 300")
|
||||||
|
pw = tok.get("pw") or ""
|
||||||
|
if pw:
|
||||||
|
logs = logs.replace(pw, "<generated, redacted>")
|
||||||
|
open(f"{EVD}/wger.log", "w").write(logs)
|
||||||
|
json.dump(REC, open(f"{EVD}/box-verdict-wger.json", "w"), indent=2, ensure_ascii=False)
|
||||||
|
say("RECORD", json.dumps({k: REC[k] for k in REC if k not in ("container", "watch")}, ensure_ascii=False))
|
||||||
|
elif phase == "remove":
|
||||||
|
say(f"remove -> {w.remove(APP)}")
|
||||||
|
g = w.guest("docker ps -a --format '{{.Names}}' | grep -i wger; echo vols:; docker volume ls --format '{{.Name}}' | grep -i wger; "
|
||||||
|
"echo stackdir:; ls -la /opt/docker/stacks/wger; echo end")
|
||||||
|
say("after remove:\n" + g)
|
||||||
|
st = w.stack(APP)
|
||||||
|
say(f"stack: deployed={st.get('deployed')} state={st.get('state')}")
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Point 9202 at the drill catalog, or put the saved controller.yaml back. `09` §6.5."""
|
||||||
|
import io, os, re, sys
|
||||||
|
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||||
|
import box_walk as w
|
||||||
|
VOL = "/var/lib/docker/volumes/felhom-controller-data/_data"
|
||||||
|
SAVE = f"{VOL}/controller.yaml.pre-r762-1008"
|
||||||
|
DRILL = "https://gitea.dooplex.hu/admin/app-catalog-drill.git"
|
||||||
|
def creds():
|
||||||
|
for l in io.open(os.path.expanduser("~/.git-credentials")).read().split("\n"):
|
||||||
|
m = re.match(r"https://(admin):([^@]+)@gitea\.dooplex\.hu", l)
|
||||||
|
if m: return m.group(1), m.group(2)
|
||||||
|
sys.exit("no admin credential")
|
||||||
|
if sys.argv[1] == "drill":
|
||||||
|
u, t = creds()
|
||||||
|
out = w.guest(f"""set -e
|
||||||
|
cp -p {VOL}/controller.yaml {SAVE}
|
||||||
|
python3 - <<'PY'
|
||||||
|
import re
|
||||||
|
p = "{VOL}/controller.yaml"; s = open(p).read()
|
||||||
|
s = re.sub(r'(^\\s+repo_url: ).*$', r'\\g<1>{DRILL}', s, count=1, flags=re.M)
|
||||||
|
s = re.sub(r'(^git:(?:\\n\\s+.*)*?\\n\\s+token: ).*$', r'\\g<1>"{t}"', s, count=1, flags=re.M)
|
||||||
|
s = re.sub(r'(^git:(?:\\n\\s+.*)*?\\n\\s+username: ).*$', r'\\g<1>"{u}"', s, count=1, flags=re.M)
|
||||||
|
open(p, "w").write(s)
|
||||||
|
PY
|
||||||
|
rm -rf {VOL}/catalog-cache {VOL}/data/catalog-cache
|
||||||
|
docker restart felhom-controller >/dev/null
|
||||||
|
grep -n 'repo_url' {VOL}/controller.yaml
|
||||||
|
""")
|
||||||
|
print(out.replace(t, "<token>"))
|
||||||
|
elif sys.argv[1] == "restore":
|
||||||
|
print(w.guest(f"""set -e
|
||||||
|
cp -p {SAVE} {VOL}/controller.yaml
|
||||||
|
rm -rf {VOL}/catalog-cache {VOL}/data/catalog-cache
|
||||||
|
docker restart felhom-controller >/dev/null
|
||||||
|
grep -n 'repo_url' {VOL}/controller.yaml; grep -c 'token: ""' {VOL}/controller.yaml || true
|
||||||
|
cmp {SAVE} {VOL}/controller.yaml && echo RESTORED-IDENTICAL"""))
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
Applying allauth_idp_oidc.0001_initial... OK
|
||||||
|
Applying allauth_idp_oidc.0002_client_default_scopes... OK
|
||||||
|
Applying allauth_idp_oidc.0003_client_allow_uri_wildcards... OK
|
||||||
|
Applying contenttypes.0002_remove_content_type_name... OK
|
||||||
|
Applying auth.0002_alter_permission_name_max_length... OK
|
||||||
|
Applying auth.0003_alter_user_email_max_length... OK
|
||||||
|
Applying auth.0004_alter_user_username_opts... OK
|
||||||
|
Applying auth.0005_alter_user_last_login_null... OK
|
||||||
|
Applying auth.0006_require_contenttypes_0002... OK
|
||||||
|
Applying auth.0007_alter_validators_add_error_messages... OK
|
||||||
|
Applying auth.0008_alter_user_username_max_length... OK
|
||||||
|
Applying auth.0009_alter_user_last_name_max_length... OK
|
||||||
|
Applying auth.0010_alter_group_name_max_length... OK
|
||||||
|
Applying auth.0011_update_proxy_permissions... OK
|
||||||
|
Applying auth.0012_alter_user_first_name_max_length... OK
|
||||||
|
Applying authtoken.0001_initial... OK
|
||||||
|
Applying authtoken.0002_auto_20160226_1747... OK
|
||||||
|
Applying authtoken.0003_tokenproxy... OK
|
||||||
|
Applying authtoken.0004_alter_tokenproxy_options... OK
|
||||||
|
Applying axes.0001_initial... OK
|
||||||
|
Applying axes.0002_auto_20151217_2044... OK
|
||||||
|
Applying axes.0003_auto_20160322_0929... OK
|
||||||
|
Applying axes.0004_auto_20181024_1538... OK
|
||||||
|
Applying axes.0005_remove_accessattempt_trusted... OK
|
||||||
|
Applying axes.0006_remove_accesslog_trusted... OK
|
||||||
|
Applying axes.0007_alter_accessattempt_unique_together... OK
|
||||||
|
Applying axes.0008_accessfailurelog... OK
|
||||||
|
Applying axes.0009_add_session_hash... OK
|
||||||
|
Applying axes.0010_accessattemptexpiration... OK
|
||||||
|
Applying gym.0001_initial... OK
|
||||||
|
Applying core.0001_initial... OK
|
||||||
|
Applying config.0001_initial... OK
|
||||||
|
Applying config.0002_auto_20190618_1617... OK
|
||||||
|
Applying config.0003_delete_languageconfig... OK
|
||||||
|
Applying sessions.0001_initial... OK
|
||||||
|
Applying weight.0001_initial... OK
|
||||||
|
Applying weight.0002_auto_20150604_2139... OK
|
||||||
|
Applying weight.0003_auto_20160416_1030... OK
|
||||||
|
Applying weight.0004_multiple_weight_entries_per_day... OK
|
||||||
|
Applying weight.0005_add_uuid... OK
|
||||||
|
Applying nutrition.0001_initial... OK
|
||||||
|
Applying nutrition.0002_auto_20170101_1538... OK
|
||||||
|
Applying nutrition.0003_auto_20170118_2308... OK
|
||||||
|
Applying nutrition.0004_auto_20200819_2310... OK
|
||||||
|
Applying nutrition.0005_logitem... OK
|
||||||
|
Applying nutrition.0006_auto_20201201_0653... OK
|
||||||
|
Applying nutrition.0007_auto_20201214_0013... OK
|
||||||
|
Applying nutrition.0008_auto_20210102_1446... OK
|
||||||
|
Applying nutrition.0009_meal_name... OK
|
||||||
|
Applying nutrition.0010_logitem_meal... OK
|
||||||
|
Applying nutrition.0011_alter_logitem_datetime... OK
|
||||||
|
Applying nutrition.0012_alter_ingredient_license_author... OK
|
||||||
|
Applying gym.0002_auto_20151003_1944... OK
|
||||||
|
Applying gym.0003_auto_20151003_2008... OK
|
||||||
|
Applying gym.0004_auto_20151003_2357... OK
|
||||||
|
Applying gym.0005_auto_20151023_1522... OK
|
||||||
|
Applying gym.0006_auto_20160214_1013... OK
|
||||||
|
Applying gym.0007_auto_20170123_0920... OK
|
||||||
|
Applying gym.0008_auto_20190618_1617... OK
|
||||||
|
Applying exercises.0001_initial... OK
|
||||||
|
Applying manager.0001_initial... OK
|
||||||
|
Applying manager.0002_auto_20150202_2040... OK
|
||||||
|
Applying manager.0004_auto_20150609_1603... OK
|
||||||
|
Applying core.0002_auto_20141225_1512... OK
|
||||||
|
Applying core.0003_auto_20150217_1554... OK
|
||||||
|
Applying core.0004_auto_20150217_1914... OK
|
||||||
|
Applying core.0005_auto_20151025_2236... OK
|
||||||
|
Applying core.0006_auto_20151025_2237... OK
|
||||||
|
Applying core.0007_repetitionunit... OK
|
||||||
|
Applying core.0008_weightunit... OK
|
||||||
|
Applying core.0009_auto_20160303_2340... OK
|
||||||
|
Applying core.0010_auto_20170403_0144... OK
|
||||||
|
Applying core.0011_auto_20201201_0653... OK
|
||||||
|
Applying core.0012_auto_20210210_1228... OK
|
||||||
|
Applying core.0013_auto_20210726_1729... OK
|
||||||
|
Applying nutrition.0013_ingredient_image... OK
|
||||||
|
Applying nutrition.0014_license_information... OK
|
||||||
|
Applying nutrition.0015_alter_ingredient_creation_date_and_more... OK
|
||||||
|
Applying nutrition.0016_alter_logitem_options_and_more... OK
|
||||||
|
Applying nutrition.0017_remove_nutritionplan_language_alter_logitem_meal... OK
|
||||||
|
Applying nutrition.0018_nutritionplan_goal_carbs_nutritionplan_goal_energy_and_more... OK
|
||||||
|
Applying nutrition.0019_alter_image_license_author_and_more... OK
|
||||||
|
Applying nutrition.0020_full_text_search... OK
|
||||||
|
Applying nutrition.0021_add_fibers_field... OK
|
||||||
|
Applying nutrition.0022_add_remote_id_increase_author_field_length... OK
|
||||||
|
Applying nutrition.0023_fiber_spelling... OK
|
||||||
|
Applying nutrition.0024_remove_ingredient_status... OK
|
||||||
|
Applying nutrition.0025_add_last_image_check... OK
|
||||||
|
Applying nutrition.0026_add_start_and_end_fields... OK
|
||||||
|
Applying nutrition.0027_prefill_end_date... OK
|
||||||
|
Applying nutrition.0028_ingredient_dietary_properties... OK
|
||||||
|
Applying nutrition.0029_ingredient_nutriscore... OK
|
||||||
|
Applying manager.0005_auto_20160303_2008... OK
|
||||||
|
Applying manager.0006_auto_20160303_2138... OK
|
||||||
|
Applying manager.0007_auto_20160311_2258... OK
|
||||||
|
Applying manager.0008_auto_20190618_1617... OK
|
||||||
|
Applying manager.0009_auto_20201202_1559... OK
|
||||||
|
Applying manager.0010_auto_20210102_1446... OK
|
||||||
|
Applying manager.0011_remove_set_exercises... OK
|
||||||
|
Applying manager.0012_auto_20210430_1449... OK
|
||||||
|
Applying manager.0013_set_comment... OK
|
||||||
|
Applying manager.0014_auto_20210717_1858... OK
|
||||||
|
Applying manager.0015_auto_20211028_1113... OK
|
||||||
|
Applying exercises.0002_auto_20150307_1841... OK
|
||||||
|
Applying exercises.0003_auto_20160921_2000... OK
|
||||||
|
Applying exercises.0004_auto_20170404_0114... OK
|
||||||
|
Applying exercises.0005_auto_20190618_1617... OK
|
||||||
|
Applying exercises.0006_auto_20201203_0203... OK
|
||||||
|
Applying exercises.0007_auto_20201203_1042... OK
|
||||||
|
Applying exercises.0008_exercisebase... OK
|
||||||
|
Applying exercises.0009_auto_20201211_0139... OK
|
||||||
|
Applying exercises.0010_auto_20201211_0205... OK
|
||||||
|
Applying exercises.0011_auto_20201214_0033... OK
|
||||||
|
Applying exercises.0012_auto_20210327_1219... OK
|
||||||
|
Applying exercises.0013_auto_20210503_1232... OK
|
||||||
|
Applying exercises.0014_exerciseimage_style... OK
|
||||||
|
Applying exercises.0015_exercise_videos... OK
|
||||||
|
Applying exercises.0016_exercisealias... OK
|
||||||
|
Applying exercises.0017_muscle_name_en... OK
|
||||||
|
Applying core.0013_userprofile_email_verified... OK
|
||||||
|
Applying core.0014_merge_20210818_1735... OK
|
||||||
|
Applying exercises.0018_delete_pending_exercises... OK
|
||||||
|
Applying exercises.0019_exercise_crowdsourcing_changes... OK
|
||||||
|
Applying manager.0016_move_to_exercise_base... OK
|
||||||
|
Applying manager.0017_alter_workoutlog_exercise_base... OK
|
||||||
|
Applying exercises.0020_historicalexerciseimage_historicalexercisevideo... OK
|
||||||
|
Applying exercises.0021_deletionlog... OK
|
||||||
|
Applying exercises.0022_alter_exercise_license_author_and_more... OK
|
||||||
|
Applying exercises.0023_make_uuid_unique... OK
|
||||||
|
Applying exercises.0024_license_information... OK
|
||||||
|
Applying exercises.0025_rename_update_date_exercise_last_update_and_more... OK
|
||||||
|
Applying exercises.0026_deletionlog_replaced_by... OK
|
||||||
|
Applying exercises.0027_alter_deletionlog_replaced_by_and_more... OK
|
||||||
|
Applying exercises.0028_add_uuid_alias_and_comments... OK
|
||||||
|
Applying exercises.0029_full_text_search... OK
|
||||||
|
Applying exercises.0030_increase_author_field_length... OK
|
||||||
|
Applying exercises.0032_rename_exercise... OK
|
||||||
|
Applying core.0015_alter_language_short_name... OK
|
||||||
|
Applying core.0016_alter_language_short_name... OK
|
||||||
|
Applying manager.0018_flexible_routines... OK
|
||||||
|
Applying manager.0019_flexible_routines_migration... OK
|
||||||
|
Applying manager.0021_flexible_routines_cleanup... OK
|
||||||
|
Applying core.0017_language_full_name_en... OK
|
||||||
|
Applying core.0018_rounding... OK
|
||||||
|
Applying core.0019_delete_daysofweek... OK
|
||||||
|
Applying core.0020_add_trophies_enabled_to_userprofile... OK
|
||||||
|
Applying core.0021_add_unit_type_to_repetitionunit... OK
|
||||||
|
Applying nutrition.0030_add_indices... OK
|
||||||
|
Applying nutrition.0031_start_weight_unit_merge... OK
|
||||||
|
Applying nutrition.0032_continue_weight_unit_merge... OK
|
||||||
|
Applying nutrition.0033_finalize_weight_unit_merge... OK
|
||||||
|
Applying nutrition.0034_ingredient_trigram_gin_index... OK
|
||||||
|
Applying nutrition.0035_add_uuids... OK
|
||||||
|
Applying nutrition.0036_alter_image_license_author_and_more... OK
|
||||||
|
Applying nutrition.0037_powersync_synced_ingredient_tables... OK
|
||||||
|
Applying measurements.0001_initial... OK
|
||||||
|
Applying measurements.0002_auto_20210722_1042... OK
|
||||||
|
Applying measurements.0003_alter_measurement_unique_together_and_more... OK
|
||||||
|
Applying measurements.0004_add_uuids... OK
|
||||||
|
Applying measurements.0005_alter_measurement_date... OK
|
||||||
|
Applying trophies.0001_initial... OK
|
||||||
|
Applying trophies.0002_load_initial_trophies... OK
|
||||||
|
Applying manager.0022_alter_rir_type... OK
|
||||||
|
Applying manager.0023_change_validators... OK
|
||||||
|
Applying manager.0024_log_and_session_uuid... OK
|
||||||
|
Applying manager.0025_change_pk_to_uuid... OK
|
||||||
|
Applying trophies.0003_migrate_context_data_uuids... OK
|
||||||
|
Applying manager.0026_change_pk_to_uuid_swap... OK
|
||||||
|
Applying core.0022_move_email_verified_to_emailaddress... OK
|
||||||
|
Applying core.0023_create_publication... OK
|
||||||
|
Applying manager.0027_cleanup_fields... OK
|
||||||
|
Applying manager.0028_backfill_session_day... OK
|
||||||
|
Applying gallery.0001_initial... OK
|
||||||
|
Applying exercises.0033_uniqueness_constraint_translations... OK
|
||||||
|
Applying exercises.0034_add_exercise_image_dimensions... OK
|
||||||
|
Applying exercises.0035_add_is_ai_generated... OK
|
||||||
|
Applying exercises.0036_add_markdown_description_field... OK
|
||||||
|
Applying exercises.0037_replace_variation_with_uuid_field... OK
|
||||||
|
Applying exercises.0038_sync_model_changes... OK
|
||||||
|
Applying exercises.0039_translation_alias_trigram_gin_index... OK
|
||||||
|
Applying exercises.0040_alter_exercise_license_author_and_more... OK
|
||||||
|
Applying core.0024_backfill_emailaddress... OK
|
||||||
|
Applying core.0025_remove_unused_fields_in_userprofile... OK
|
||||||
|
Applying core.0026_alter_userprofile_birthdate_alter_userprofile_height... OK
|
||||||
|
Applying core.0027_powersync_publication... OK
|
||||||
|
Applying core.0028_longlivedsession... OK
|
||||||
|
Applying core.0029_userprofile_timezone... OK
|
||||||
|
Applying easy_thumbnails.0001_initial... OK
|
||||||
|
Applying easy_thumbnails.0002_thumbnaildimensions... OK
|
||||||
|
Applying mailer.0001_initial... OK
|
||||||
|
Applying mailer.0002_auto_20190618_1617... OK
|
||||||
|
Applying mailer.0003_auto_20201201_0653... OK
|
||||||
|
Applying manager.0029_alter_workoutsession_options_and_more... OK
|
||||||
|
Applying measurements.0006_health_sync... OK
|
||||||
|
Applying measurements.0007_migrate_weight... OK
|
||||||
|
Applying measurements.0008_dynamic_type... OK
|
||||||
|
Applying mfa.0001_initial... OK
|
||||||
|
Applying mfa.0002_authenticator_timestamps... OK
|
||||||
|
Applying mfa.0003_authenticator_type_uniq... OK
|
||||||
|
Applying sites.0001_initial... OK
|
||||||
|
Applying sites.0002_alter_domain_unique... OK
|
||||||
|
Applying socialaccount.0001_initial... OK
|
||||||
|
Applying socialaccount.0002_token_max_lengths... OK
|
||||||
|
Applying socialaccount.0003_extra_data_default_dict... OK
|
||||||
|
Applying socialaccount.0004_app_provider_id_settings... OK
|
||||||
|
Applying socialaccount.0005_socialtoken_nullable_app... OK
|
||||||
|
Applying socialaccount.0006_alter_socialaccount_extra_data... OK
|
||||||
|
Applying token_blacklist.0001_initial... OK
|
||||||
|
Applying token_blacklist.0002_outstandingtoken_jti_hex... OK
|
||||||
|
Applying token_blacklist.0003_auto_20171017_2007... OK
|
||||||
|
Applying token_blacklist.0004_auto_20171017_2013... OK
|
||||||
|
Applying token_blacklist.0005_remove_outstandingtoken_jti... OK
|
||||||
|
Applying token_blacklist.0006_auto_20171017_2113... OK
|
||||||
|
Applying token_blacklist.0007_auto_20171017_2214... OK
|
||||||
|
Applying token_blacklist.0008_migrate_to_bigautofield... OK
|
||||||
|
Applying token_blacklist.0010_fix_migrate_to_bigautofield... OK
|
||||||
|
Applying token_blacklist.0011_linearizes_history... OK
|
||||||
|
Applying token_blacklist.0012_alter_outstandingtoken_user... OK
|
||||||
|
Applying token_blacklist.0013_alter_blacklistedtoken_options_and_more... OK
|
||||||
|
Applying weight.0006_delete_weightentry... OK
|
||||||
|
*** Using settings from env: settings.main
|
||||||
|
Installed 1 object(s) from 1 fixture(s)
|
||||||
|
Installed 33 object(s) from 1 fixture(s)
|
||||||
|
Installed 7 object(s) from 1 fixture(s)
|
||||||
|
Installed 3 object(s) from 1 fixture(s)
|
||||||
|
Installed 5 object(s) from 1 fixture(s)
|
||||||
|
Installed 8 object(s) from 1 fixture(s)
|
||||||
|
Installed 6 object(s) from 1 fixture(s)
|
||||||
|
Installed 1 object(s) from 1 fixture(s)
|
||||||
|
Installed 12 object(s) from 1 fixture(s)
|
||||||
|
Installed 16 object(s) from 1 fixture(s)
|
||||||
|
Installed 8 object(s) from 1 fixture(s)
|
||||||
|
Installed 872 object(s) from 1 fixture(s)
|
||||||
|
Installed 2429 object(s) from 1 fixture(s)
|
||||||
|
Installed 1 object(s) from 1 fixture(s)
|
||||||
|
Installed 1 object(s) from 1 fixture(s)
|
||||||
|
Installed 1 object(s) from 1 fixture(s)
|
||||||
|
*** Using settings from env: settings.main
|
||||||
|
*** Password for user admin was reset to '<image-default, redacted>'
|
||||||
|
Installed 3 object(s) from 1 fixture(s)
|
||||||
|
Running in production mode, running collectstatic now
|
||||||
|
level=INFO ts=2026-10-08 09:54:12,682 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
|
||||||
|
|
||||||
|
11362 static files copied to '/home/wger/static', 11362 post-processed.
|
||||||
|
Performing database migrations
|
||||||
|
level=INFO ts=2026-10-08 09:54:32,571 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
|
||||||
|
System check identified some issues:
|
||||||
|
|
||||||
|
WARNINGS:
|
||||||
|
?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies.
|
||||||
|
HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS.
|
||||||
|
Operations to perform:
|
||||||
|
Apply all migrations: account, actstream, allauth_idp_oidc, auth, authtoken, axes, config, contenttypes, core, easy_thumbnails, exercises, gallery, gym, mailer, manager, measurements, mfa, nutrition, sessions, sites, socialaccount, token_blacklist, trophies, weight
|
||||||
|
Running migrations:
|
||||||
|
No migrations to apply.
|
||||||
|
Your models in app(s): 'exercises', 'gallery' have changes that are not yet reflected in a migration, and so won't be applied.
|
||||||
|
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||||
|
level=INFO ts=2026-10-08 09:54:35,564 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
|
||||||
|
System check identified some issues:
|
||||||
|
|
||||||
|
WARNINGS:
|
||||||
|
?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies.
|
||||||
|
HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS.
|
||||||
|
Set site URL to fitness.enkisfelhom.hu
|
||||||
|
Using gunicorn on port 8000...
|
||||||
|
level=INFO ts=2026-10-08 09:54:38,089 module=apps path=/home/wger/.local/lib/python3.12/site-packages/axes/apps.py line=53 message=AXES: BEGIN version 8.3.1, blocking by username
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Starting gunicorn 26.1.0
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Listening at: http://0.0.0.0:8000 (58)
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Using worker: sync
|
||||||
|
[2026-10-08 09:54:38 +0200] [59] [INFO] Booting worker with pid: 59
|
||||||
|
[2026-10-08 09:54:38 +0200] [60] [INFO] Booting worker with pid: 60
|
||||||
|
[2026-10-08 09:54:38 +0200] [58] [INFO] Control socket listening at /home/wger/.gunicorn/gunicorn.ctl
|
||||||
|
level=WARNING ts=2026-10-08 09:54:53,158 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=INFO ts=2026-10-08 09:54:56,132 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=295 message=AXES: Successful login by {username: "********************", ip_address: "********************", user_agent: "curl/8.14.1", path_info: "/en/user/login"}.
|
||||||
|
level=INFO ts=2026-10-08 09:54:56,134 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=425 message=AXES: Cleaned up 0 expired access attempts from database that were older than 2026-10-08 07:49:56.005561+00:00
|
||||||
|
level=WARNING ts=2026-10-08 09:54:56,295 module=log path=/home/wger/.local/lib/python3.12/site-packages/django/utils/log.py line=249 message=Forbidden: /api/v2/weightentry/
|
||||||
|
level=INFO ts=2026-10-08 09:54:56,491 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=295 message=AXES: Successful login by {username: "********************", ip_address: "********************", user_agent: "curl/8.14.1", path_info: "/en/user/login"}.
|
||||||
|
level=INFO ts=2026-10-08 09:54:56,492 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=425 message=AXES: Cleaned up 0 expired access attempts from database that were older than 2026-10-08 07:49:56.371620+00:00
|
||||||
|
level=INFO ts=2026-10-08 09:54:57,028 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=295 message=AXES: Successful login by {username: "********************", ip_address: "********************", user_agent: "curl/8.14.1", path_info: "/en/user/login"}.
|
||||||
|
level=INFO ts=2026-10-08 09:54:57,029 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=425 message=AXES: Cleaned up 0 expired access attempts from database that were older than 2026-10-08 07:49:56.907310+00:00
|
||||||
|
level=WARNING ts=2026-10-08 09:55:23,254 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:55:53,725 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:56:23,813 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:56:53,908 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:57:24,005 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:57:54,097 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:58:24,191 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:58:54,279 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:59:24,378 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 09:59:54,478 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:00:24,571 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:00:54,667 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:01:24,758 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:01:54,852 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:02:24,938 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:02:55,018 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:03:25,106 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:03:55,203 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:04:25,294 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
|
level=WARNING ts=2026-10-08 10:04:55,381 module=wsgi path=/home/wger/.local/lib/python3.12/site-packages/gunicorn/http/wsgi.py line=450 message=WSGI app sent body bytes on a no-body response (method=HEAD status=200); dropping per RFC 9110.
|
||||||
@@ -32,6 +32,8 @@ The full text of every row below: `git show b2dce901b2:documentation/backlog/OPE
|
|||||||
|
|
||||||
| Row | What | Closed | Evidence |
|
| Row | What | Closed | Evidence |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| **R-415** | **The hub enforced uniqueness on `customer_id` only: two customers could be given the same or a nested domain.** (P3) | CLOSED 2026-10-08 — FIXED on hub main (ships with the next hub release): create and edit refuse a domain equal to, containing or under another customer's, or under felhom.eu | `hub/internal/store/domain_conflict.go`, `TestR415_CreateAndEditRefuseConflictingDomain` (red-proved); re-filed the same day after the 2026-10-03 triage lost it; full text `git show 78121aa475:documentation/backlog/OPEN-ITEMS.md`. |
|
||||||
|
| **R-762** | **wger served no CSS, no JavaScript and no uploaded photo; and ran Django's development server.** (P3) | CLOSED 2026-10-08 — both halves proven and pushed: the files (catalog `cf1ed43`, 2026-10-06) and gunicorn with 2 workers (catalog `eec9a0d`, CI job 1546 success; operator ruling 3, decision 182, for the drill-catalog write) | Bench 9401: anon 170 MiB = 44 % of 384M, 0 kills, 0 restarts over 606 s; 9202 (drill catalog, fresh install through the product): 2 × „Booting worker", anon 246 MiB = 64 %, 0 kills, 0 restarts over 600 s / 10,944 requests, login 200, 3 CSS 200, photo 201 → 200, unknown static 404 (control). No ladder step applies (images unchanged; `ladder.check_entry` refuses a same-digest re-test; `09` §5.4 renders the catalog template). Evidence `audits/day-2026-10-08/r762/`. Follow-ups: R-905 (static files in backups); `mem_request` decision D10. wger stays `lifecycle: hidden`. |
|
||||||
| **R-177** | **There was no operator-triggerable „run the fill check now" path.** (P4) | CLOSED 2026-10-08 — NO LONGER TRUE: since controller v0.297.0 (R-363) the fill check also runs every 10 minutes, so no door is needed for it | `felhom-controller/controller/cmd/controller/main.go:1579` (`sched.Every("fill-watch-interval", …)`), `:2478` (`fillWatchInterval = 10 * time.Minute`), pinned by `cmd/controller/r363_fillwatch_interval_test.go`; each run logs „checked N filesystem(s)" (readable through the hub's controller-log pull). Design `audits/day-2026-10-08/design-R-314-279-177.md`. |
|
| **R-177** | **There was no operator-triggerable „run the fill check now" path.** (P4) | CLOSED 2026-10-08 — NO LONGER TRUE: since controller v0.297.0 (R-363) the fill check also runs every 10 minutes, so no door is needed for it | `felhom-controller/controller/cmd/controller/main.go:1579` (`sched.Every("fill-watch-interval", …)`), `:2478` (`fillWatchInterval = 10 * time.Minute`), pinned by `cmd/controller/r363_fillwatch_interval_test.go`; each run logs „checked N filesystem(s)" (readable through the hub's controller-log pull). Design `audits/day-2026-10-08/design-R-314-279-177.md`. |
|
||||||
| **R-298** | **The `/storage` page's unregistered list filtered on `role==='user-data'`, so a drive that is also the backup target could never be registered.** (P3) | CLOSED 2026-10-08 — NOT REPRODUCIBLE / NOT TRUE: the agent's role comes from the storage type and backing disk and never from being the backup target, so a backup-target drive on a non-system disk IS user-data | `felhom-agent/internal/storage/role.go:172-186` (unchanged since 2026-07-13); `internal/localapi/disks.go:212-233` and `:1239-1249`; August topology `audits/evidence-rehearsal-2026-08-09/GATE0-demo-hp-before.txt:47-84` → user-data; read-only today: demo-felhom `felhom-backup` on `sdb` (root on `sda`). Side fix on controller main `a40729a`: no eject/format button on a backup-target drive (the agent refuses the eject, 403). |
|
| **R-298** | **The `/storage` page's unregistered list filtered on `role==='user-data'`, so a drive that is also the backup target could never be registered.** (P3) | CLOSED 2026-10-08 — NOT REPRODUCIBLE / NOT TRUE: the agent's role comes from the storage type and backing disk and never from being the backup target, so a backup-target drive on a non-system disk IS user-data | `felhom-agent/internal/storage/role.go:172-186` (unchanged since 2026-07-13); `internal/localapi/disks.go:212-233` and `:1239-1249`; August topology `audits/evidence-rehearsal-2026-08-09/GATE0-demo-hp-before.txt:47-84` → user-data; read-only today: demo-felhom `felhom-backup` on `sdb` (root on `sda`). Side fix on controller main `a40729a`: no eject/format button on a backup-target drive (the agent refuses the eject, 403). |
|
||||||
| **R-900** | **The website's FAQ said the household's data is not with a third party, while copies and traffic go to processors.** (P2) | CLOSED 2026-10-08 — PUBLISHED: the GDPR answer on `gyik.html` and `en/faq.html` (visible text and structured data) now names the box, the encrypted copies at Hetzner in the EU, and Cloudflare's view of remote-access traffic; text approved by the operator in chat (`09` §3 decision 180) | website commit `b2dce901`; live read-back 2026-10-08: the new sentence 2× on each page (visible + JSON-LD), the old „nem harmadik félnél" 0×; rest of the site searched (ASCII fragments, positive control) — no other page makes the claim. Left for R-813: the contact form's consent line „harmadik félnek nem adjuk ki", which the consent draft already replaces. |
|
| **R-900** | **The website's FAQ said the household's data is not with a third party, while copies and traffic go to processors.** (P2) | CLOSED 2026-10-08 — PUBLISHED: the GDPR answer on `gyik.html` and `en/faq.html` (visible text and structured data) now names the box, the encrypted copies at Hetzner in the EU, and Cloudflare's view of remote-access traffic; text approved by the operator in chat (`09` §3 decision 180) | website commit `b2dce901`; live read-back 2026-10-08: the new sentence 2× on each page (visible + JSON-LD), the old „nem harmadik félnél" 0×; rest of the site searched (ASCII fragments, positive control) — no other page makes the claim. Left for R-813: the contact form's consent line „harmadik félnek nem adjuk ki", which the consent draft already replaces. |
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+8
-1
@@ -1,9 +1,16 @@
|
|||||||
## Unreleased (2026-10-08) — an alarm when a box never backs up off-site because its escrow is pending (R-243; `09` §3 decision 179); a deleted customer's audit rows go after 1 year (R-901; decision 181); the operator's older-recovery-package mail (R-304; decision 183) — ships with tomorrow's hub release
|
## Unreleased (2026-10-08) — an alarm when a box never backs up off-site because its escrow is pending (R-243; `09` §3 decision 179); a deleted customer's audit rows go after 1 year (R-901; decision 181); the operator's older-recovery-package mail (R-304; decision 183); no duplicate or nested customer domain (R-415, R-138 option B) — ships with tomorrow's hub release
|
||||||
|
|
||||||
**Operator action on deploy: none.** Expect ONE `offsite_escrow_pending` mail for **Tester 2** on the first sweep after
|
**Operator action on deploy: none.** Expect ONE `offsite_escrow_pending` mail for **Tester 2** on the first sweep after
|
||||||
the deploy: its latest report (2026-10-04) says off-site ON, escrow `pending`, no successful run ever — the state the
|
the deploy: its latest report (2026-10-04) says off-site ON, escrow `pending`, no successful run ever — the state the
|
||||||
operator believes it is in (decision 170).
|
operator believes it is in (decision 170).
|
||||||
|
|
||||||
|
- **R-415 / R-138 option B (operator ruling 2026-09-14, `01` §7 — every customer has their own domain):** the create and
|
||||||
|
edit paths refuse a domain that equals, contains or lies under another customer's, or is under `felhom.eu`, BEFORE
|
||||||
|
anything is generated or provisioned; the form re-renders with the submitted values and one sentence; a store error
|
||||||
|
refuses too. Label-boundary matching, case-insensitive, trailing dot ignored. `store.DomainConflict`; test
|
||||||
|
`TestR415_CreateAndEditRefuseConflictingDomain` (red-proved: without the create guard „b1 with example.hu was
|
||||||
|
created"). Live data read first (read-only DB copy, deleted): the four customers' domains are distinct and none is
|
||||||
|
under felhom.eu. The form's example and one old test fixture said `kovacs.felhom.eu` — corrected to `kovacs.hu`.
|
||||||
- **R-304 option C (decision 183):** new event type `recovery_older_package` (sent by the controller of the same day when
|
- **R-304 option C (decision 183):** new event type `recovery_older_package` (sent by the controller of the same day when
|
||||||
a household's code opens, or may open, an older sealed escrow package): in `allowedEventTypes` and
|
a household's code opens, or may open, an older sealed escrow package): in `allowedEventTypes` and
|
||||||
`notify.operatorOnlyEvents`, no household text. Test `TestR304_RecoveryOlderPackageIsAllowlistedAndOperatorOnly`
|
`notify.operatorOnlyEvents`, no household text. Test `TestR304_RecoveryOlderPackageIsAllowlistedAndOperatorOnly`
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// R-415 / R-138 option B (2026-10-08; operator ruling 2026-09-14, `01` §7: every customer has their OWN domain, never a
|
||||||
|
// name under felhom.eu). The hub enforced uniqueness on customer_id only, so two customers could be given the same
|
||||||
|
// domain — or one inside the other, a shared zone in all but name — silently. DomainConflict answers, for a domain about
|
||||||
|
// to be saved for customerID, which rule it breaks ("" = none):
|
||||||
|
//
|
||||||
|
// - "felhom.eu" — the domain is felhom.eu or a name under it;
|
||||||
|
// - "<other id>" — another customer's domain equals it, contains it, or lies under it.
|
||||||
|
//
|
||||||
|
// Matching is case-insensitive, ignores a trailing dot, and is on LABEL boundaries („notexample.hu" is not under
|
||||||
|
// „example.hu"). An empty domain conflicts with nothing. Pinned by TestR415_* (domain_conflict_test.go) and the
|
||||||
|
// handler test TestR415_CreateAndEditRefuseConflictingDomain.
|
||||||
|
func (s *Store) DomainConflict(customerID, domain string) (string, error) {
|
||||||
|
d := normDomain(domain)
|
||||||
|
if d == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if d == "felhom.eu" || strings.HasSuffix(d, ".felhom.eu") {
|
||||||
|
return "felhom.eu", nil
|
||||||
|
}
|
||||||
|
rows, err := s.db.Query(`SELECT customer_id, domain FROM customer_configs WHERE customer_id != ? AND domain != ''`, customerID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id, other string
|
||||||
|
if err := rows.Scan(&id, &other); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
o := normDomain(other)
|
||||||
|
if o == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if d == o || strings.HasSuffix(d, "."+o) || strings.HasSuffix(o, "."+d) {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func normDomain(d string) string {
|
||||||
|
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(d)), ".")
|
||||||
|
}
|
||||||
@@ -741,6 +741,18 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// R-415 / R-138 option B: a domain equal to, inside or containing another customer's, or under felhom.eu, is refused
|
||||||
|
// BEFORE anything is generated or provisioned.
|
||||||
|
if msg := s.domainConflictMessage(customerID, r.FormValue("domain")); msg != "" {
|
||||||
|
s.renderConfigForm(w, r, true, &store.CustomerConfig{
|
||||||
|
CustomerID: customerID,
|
||||||
|
CustomerName: r.FormValue("customer_name"),
|
||||||
|
Domain: r.FormValue("domain"),
|
||||||
|
Email: r.FormValue("email"),
|
||||||
|
}, nil, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Generate credentials.
|
// Generate credentials.
|
||||||
//
|
//
|
||||||
// R-597: the Owner passphrase follows the language the operator is choosing ON THIS FORM, not
|
// R-597: the Owner passphrase follows the language the operator is choosing ON THIS FORM, not
|
||||||
@@ -861,6 +873,13 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
|
|||||||
s.renderConfigForm(w, r, false, cfg, submitted, "Display Name and Domain are required.")
|
s.renderConfigForm(w, r, false, cfg, submitted, "Display Name and Domain are required.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// R-415 / R-138 option B — the same guard on edit (a customer's own current domain never conflicts with itself).
|
||||||
|
if msg := s.domainConflictMessage(customerID, cfg.Domain); msg != "" {
|
||||||
|
var submitted map[string]interface{}
|
||||||
|
_ = json.Unmarshal([]byte(buildConfigJSON(r)), &submitted)
|
||||||
|
s.renderConfigForm(w, r, false, cfg, submitted, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfg.ConfigJSON = buildConfigJSON(r)
|
cfg.ConfigJSON = buildConfigJSON(r)
|
||||||
|
|
||||||
@@ -1769,3 +1788,22 @@ func (s *Server) handleGeoDisable(w http.ResponseWriter, r *http.Request, custom
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Geo-restriction removed from Cloudflare."})
|
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Geo-restriction removed from Cloudflare."})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// domainConflictMessage is the operator's sentence for a refused domain ("" = allowed). A store error refuses too
|
||||||
|
// (fail closed: the save can be retried; a duplicate domain cannot be undone once boxes use it).
|
||||||
|
func (s *Server) domainConflictMessage(customerID, domain string) string {
|
||||||
|
other, err := s.store.DomainConflict(customerID, domain)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("[ERROR] domain check for %s failed: %v — save refused", customerID, err)
|
||||||
|
return "The domain could not be checked against the other customers — nothing was saved. Try again."
|
||||||
|
}
|
||||||
|
switch other {
|
||||||
|
case "":
|
||||||
|
return ""
|
||||||
|
case "felhom.eu":
|
||||||
|
return fmt.Sprintf("Domain %q is under felhom.eu — every customer has their own domain (01 §7). Nothing was saved.", strings.TrimSpace(domain))
|
||||||
|
default:
|
||||||
|
s.logger.Printf("[WARN] domain %q for %s refused: it overlaps customer %s's domain (R-415)", strings.TrimSpace(domain), customerID, other)
|
||||||
|
return fmt.Sprintf("Domain %q equals, contains or lies under the domain of customer %q — every customer has their own domain (01 §7). Nothing was saved.", strings.TrimSpace(domain), other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ func TestConfigUpdate_DebugSurvivesRebuild_OffsiteUntouched(t *testing.T) {
|
|||||||
|
|
||||||
const id = "cust-dbg"
|
const id = "cust-dbg"
|
||||||
if err := st.SaveCustomerConfig(&store.CustomerConfig{
|
if err := st.SaveCustomerConfig(&store.CustomerConfig{
|
||||||
CustomerID: id, CustomerName: "Kovács", Domain: "kovacs.felhom.eu", ConfigJSON: "{}",
|
CustomerID: id, CustomerName: "Kovács", Domain: "kovacs.hu", ConfigJSON: "{}",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("seed customer: %v", err)
|
t.Fatalf("seed customer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) First save WITH offsite enabled → provisions + merges the descriptor. No debug yet.
|
// 1) First save WITH offsite enabled → provisions + merges the descriptor. No debug yet.
|
||||||
const offsiteForm = "customer_name=Kov%C3%A1cs&domain=kovacs.felhom.eu&dr_tier=on&offsite_enabled=on&offsite_type=shared&offsite_quota_gb=50"
|
const offsiteForm = "customer_name=Kov%C3%A1cs&domain=kovacs.hu&dr_tier=on&offsite_enabled=on&offsite_type=shared&offsite_quota_gb=50"
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
s.handleConfigUpdate(w, postForm("/configs/"+id+"/edit", offsiteForm), id)
|
s.handleConfigUpdate(w, postForm("/configs/"+id+"/edit", offsiteForm), id)
|
||||||
if w.Code != http.StatusSeeOther {
|
if w.Code != http.StatusSeeOther {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-415 / R-138 option B: every customer has their own domain (`01` §7). The CONSEQUENCE asserted: a refused create
|
||||||
|
// stores nothing; an allowed one is stored; an edit keeping its own domain is allowed.
|
||||||
|
// RED-PROOF: delete the domainConflictMessage block in handleConfigCreate → „b" with „example.hu" is created → FAILS.
|
||||||
|
func TestR415_CreateAndEditRefuseConflictingDomain(t *testing.T) {
|
||||||
|
s, st := newTestServer(t)
|
||||||
|
post := func(id, domain string) *httptest.ResponseRecorder {
|
||||||
|
form := url.Values{"customer_id": {id}, "customer_name": {"T " + id}, "email": {"t@example.org"}, "domain": {domain}}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/configs/new", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
s.handleConfigCreate(rr, req)
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
if rr := post("a", "example.hu"); rr.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("create a: %d %s", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
for _, c := range []struct{ id, domain string }{{"b1", "example.hu"}, {"b2", "x.EXAMPLE.hu."}, {"b3", "hu"}, {"b4", "t1.felhom.eu"}, {"b5", "felhom.eu"}} {
|
||||||
|
rr := post(c.id, c.domain)
|
||||||
|
if rr.Code == http.StatusSeeOther {
|
||||||
|
t.Errorf("%s with %q was created — it must be refused", c.id, c.domain)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rr.Body.String(), "Nothing was saved") {
|
||||||
|
t.Errorf("%s with %q: the refusal must say nothing was saved; got %d", c.id, c.domain, rr.Code)
|
||||||
|
}
|
||||||
|
if got, _ := st.GetCustomerConfig(c.id); got != nil {
|
||||||
|
t.Errorf("%s with %q: a config was stored", c.id, c.domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range []struct{ id, domain string }{{"c1", "example2.hu"}, {"c2", "notexample.hu"}} {
|
||||||
|
if rr := post(c.id, c.domain); rr.Code != http.StatusSeeOther {
|
||||||
|
t.Errorf("%s with %q must be allowed; got %d %s", c.id, c.domain, rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Edit: „a" keeping its own domain is allowed; „c1" moving under „a"'s is refused and keeps its old domain.
|
||||||
|
edit := func(id, domain string) *httptest.ResponseRecorder {
|
||||||
|
form := url.Values{"customer_name": {"T " + id}, "email": {"t@example.org"}, "domain": {domain}}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/configs/"+id, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
s.handleConfigUpdate(rr, req, id)
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
if rr := edit("a", "example.hu"); strings.Contains(rr.Body.String(), "Nothing was saved") {
|
||||||
|
t.Errorf("editing a with its own domain must not conflict with itself: %s", rr.Body.String())
|
||||||
|
}
|
||||||
|
if rr := edit("c1", "shop.example.hu"); !strings.Contains(rr.Body.String(), "Nothing was saved") {
|
||||||
|
t.Errorf("moving c1 under a's domain must be refused; got %d", rr.Code)
|
||||||
|
}
|
||||||
|
if got, _ := st.GetCustomerConfig("c1"); got == nil || got.Domain != "example2.hu" {
|
||||||
|
t.Errorf("c1's domain must stay example2.hu after a refused edit; got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<label for="domain">Domain *</label>
|
<label for="domain">Domain *</label>
|
||||||
<input type="text" id="domain" name="domain"
|
<input type="text" id="domain" name="domain"
|
||||||
value="{{.Config.Domain}}"
|
value="{{.Config.Domain}}"
|
||||||
placeholder="e.g. kovacs.felhom.eu"
|
placeholder="e.g. kovacs.hu (the customer's own domain)"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user