diff --git a/REPORT-day-2026-10-08.md b/REPORT-day-2026-10-08.md new file mode 100644 index 00000000..9cc8c85e --- /dev/null +++ b/REPORT-day-2026-10-08.md @@ -0,0 +1,42 @@ +# REPORT — 2026-10-08 (day): six parts, tonight's kernel night untouched + +(Written as `REPORT-day-2026-10-08.md` because another session worked in this clone today — the website refresh.) + +| Part | Result | +|---|---| +| **A** — a daytime press never cancels the night (R-899) | **Done on main, ships tomorrow.** Controller `6d07ca2` (ledger; red-proved), agent `4c69c25` (no OS leg after a press; the "ring 1" label fixed). The household "tonight" mail case is removed by the rule; no hub change needed. | +| **B** — the old recovery code (R-304) | **Honesty fix done on main, ships tomorrow** — but in the agent (`91b9405`) and the controller (`75b3b39`), not the hub: the hub never sees the code (zero-knowledge), so it cannot check a row; it already reports what it withheld. Design with two questions: `documentation/audits/day-2026-10-08/design-R-304.md`. | +| **C** — alarm when a box never backs up off-site (R-243) | **Done on hub main `b119301c`, ships tomorrow.** 7 days (decision 179). Will fire once for Tester 2 after the deploy. `08` §6.3 updated. | +| **D** — wger's real web server (R-762) | **Half done.** Bench: 2 workers, 43–44 % memory, 0 kills, 0 restarts in 10 min, login/CSS/photo 200. **9202 not done:** the drill-catalog write was refused by the permission check; nothing pushed. No ladder step applies (same images). | +| **E** — legal drafts (R-813) | **Done, not published.** `documentation/legal/`: ÁSZF, privacy notice, imprint, consent text; 98 placeholders, 20 guesses listed. Two findings filed (R-900, R-901). | +| **F** — DooPlex backup failure mail (R-232 a) | **Done with your yes.** One test mail reached the inbox; no backup started. `documentation/audits/day-2026-10-08/r232/`. | + +**Rows: 127 before → 130 after. Opened 2 (R-900, R-901). Closed 0.** (The third new row, R-902, is the parallel +website session's.) + +**Waits for tomorrow's releases:** +- **controller:** R-899 (ledger, `trigger=manual`), R-304 (424 → "we do not know", hu + en). MinAgent unchanged. +- **agent:** R-899 (no OS leg after a press; after-boot ring label), R-304 (424 `older_unchecked`). Binary only. +- **hub:** R-243 `offsite_escrow_pending` (expect one Tester 2 mail). +- **catalog:** nothing (Part D not pushed). + +**Operator rulings recorded first** (`09` §3): 177 (R-899 option A), 178 (today: no touch of the kernel night). CC's +pick: 179 (R-243's 7-day line — operator may reverse). + +**Machines:** demo-hp, demo-felhom, Tester 1: read only (two log/state reads on demo-hp). Bench 9401: started and +stopped again by the Part D helper; its project removed. 9202: read only. DooPlex: Part F only (one script, one key file). +Hub: read only (a copy of its database, with the `-wal`, read and deleted). No release, no deploy, no reboot, no prune. + +**CI (by head commit):** controller 1528, 1531; agent 1529, 1530; felhom.eu 1532 — all success. + +**Seen, not fixed:** the recovery screen's 14 older messages are Hungarian-only (appended to R-516). A side note from +Part D: 9202's `recipe-importer` was removed by something else between 08:0x and 08:35 — not by this session. + +## Decisions for you + +1. **Part D on 9202 — may CC write to the shared drill catalog** (bring it up to date, un-hide wger there, point 9202 at + it), as the 2026-10-06 sessions did? **My pick: yes** — it is a test catalog, and wger stays hidden on the real one. + *If you do nothing:* wger keeps the slower development server, and it stays hidden. +2. **R-304 — keep the promise "old backups stay recoverable" as worded, and add an operator mail when a household's + code opens an old package?** **My pick: yes to the mail** (small; it makes "contact support" true in practice). + *If you do nothing:* nothing is built; you hear only when a household writes to you. diff --git a/STATUS.md b/STATUS.md index 5ac12e9b..aaeb9b29 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,8 +2,23 @@ **Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.** -**Updated 2026-10-08 06:50: hub 0.143.1; demo-hp, demo-felhom and Tester 1 run agent 0.153.0 and controller 0.303.0. -The open-items list is at 127. Report: `REPORT.md`.** +**Updated 2026-10-08 (day): 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: `REPORT.md`, +`REPORT-day-2026-10-08.md`.** + +## Day (2026-10-08): fixes built for tomorrow, the old-code answer made honest, legal drafts + +- **A daytime "back up now" no longer cancels the night** (your answer A). Built and tested; it ships tomorrow. +- **The old recovery code:** when the box could not try every older package, the screen no longer says the code is + wrong. It says "we do not know" and sends the household to you. Ships tomorrow. A one-page plan has two questions. +- **New alarm:** a box with off-site on and the key step never done now mails you after 7 days. It ships tomorrow and + will fire once for Tester 2 (its key step was never done). +- **DooPlex's own backup now mails you when it fails** (your yes). One test mail reached your inbox. +- **wger's proper web server works on the test bench** (44 % of its memory, no crash, pages and photos load). The second + test, on the scratch box, was stopped: it needs a write to the shared test catalog that the system refused. +- **Legal pages: first drafts only**, not published: terms, privacy notice, imprint, and the contact-form consent text. + +**Needs you:** see the two decisions at the end of `REPORT-day-2026-10-08.md`. ## Day (2026-10-08): the website catches up, and speaks English diff --git a/documentation/audits/day-2026-10-08/design-R-304.md b/documentation/audits/day-2026-10-08/design-R-304.md new file mode 100644 index 00000000..3b51c696 --- /dev/null +++ b/documentation/audits/day-2026-10-08/design-R-304.md @@ -0,0 +1,50 @@ +# R-304 — the household's old recovery code and the retained packages: a one-page design (2026-10-08) + +**Status:** design only. Nothing here is built except today's honesty fix (below). Customer data and promises are the +operator's: they are the two questions at the end. + +## Where it stands (read in source today, not from the row) + +- **Retention works** and the material opens the old store (drill 2026-08-12, `audits/DRILL-retained-key-2026-08-12.md`). +- **R-311 shipped** (hub v0.103.0, agent v0.129.0, controller v0.214.0): after the current package refuses a code, the + agent fetches the retained packages (`GET /hosts//escrow/retained`, self-scoped, cap 16) and tries up to 6. If one + opens, the screen says „the code is correct, it opens an earlier package; contact support" (HTTP 422). +- **R-312 is DECIDED (2026-08-13): no in-product route from the recovery screen to a set-aside store** — „re-evaluate on + a real customer request". So retention is an **operator-only** capability today, by decision. +- **What was still false until today:** the agent answered „the code did not open the sealed bundle" (400) also when it + had NOT tried every earlier package — the hub withheld rows (no key material, over the cap), a package was malformed, + the 6-try cap stopped the loop, or the retained list could not be read. **Fixed on main today** (agent 424 + `older_unchecked`, controller `RecoveryOlderUnchecked`, Hungarian + English: „we do not know whether your code is + wrong … contact support"). Ships with tomorrow's releases. The fix is in the agent and the controller, not the hub: + the hub never sees the code (zero-knowledge, `07` §2), so it cannot check a row; it already reports what it withheld + (`unopenable_count`, `truncated_count`), and the agent now counts those. + +## „Which package?" — the question is smaller than it looked + +Each escrow ceremony seals with a NEW recovery code (the household is shown it once). A code opens only the package it +sealed. So when a household holds several old codes, **each code selects its own package** — no list, no choice screen. +The agent already tries newest-superseded first. The only real limits are the two caps (16 served, 6 tried), which +today's fix turns from a silent „wrong code" into an honest „not all checked". + +## Options for really serving the old copy + +| | What | Costs | Customer data / promise | +|---|---|---|---| +| **A** | Keep it operator-only (R-312). The screen's „contact support" is the route. | Nothing more. The operator needs SQLite, `age` and a shell (the drill's §4) — slow, error-prone, undocumented as a runbook. | No change. | +| **B** | In-product: when a retained package opens and carries a repository password, the screen offers a READ-ONLY browse of the old store (list + download), never a restore into place. | New surface: the old store's location (moved aside / orphaned, R-241), a second repository password in memory, a second browse path. ~2 sessions + a drill. | Changes a promise (the household can reach old history alone). **Reverses R-312** — operator only. | +| **C** | Operator-assisted, first slice: when the agent answers 422 (opens retained) or 424 (not all checked), the controller sends ONE operator event naming the box and the package date; plus a runbook „open a retained package for a household" (the drill's §4 written down, the household types the code on its own box). | Small: one event type (operator-only), one runbook. ~½ session. | No new promise; makes the existing „contact support" true in practice. | + +**Pick: C now; B only on R-312's own trigger** (a real customer asks). C makes the sentence the screen already says — +„contact support" — something the operator can act on the same day, without reversing a decision. + +**First slice of C:** controller: on `RecoveryCodeOpensRetained` / `RecoveryOlderUnchecked`, send `recovery_retained_needed` +(operator-only, warning, once per box per day) with the package date and the class — never the code. Hub: allowlist + +`operatorOnlyEvents` in the same commit. Docs: `runbooks/RUNBOOK-open-retained-package.md` from the drill's §4. + +## Two questions for the operator + +1. **May the product keep promising, in the capability map and the countdown banner, that old backups „stay + recoverable"?** Today that is true only with your hands. *If you do nothing:* the promise stays worded as it is, and + the honest route is „contact support" (A/C). +2. **Do you want C's first slice built (an operator mail when a household's code opens — or may open — an old + package)?** *If you do nothing:* nothing is built; you learn of such a household only when they write to you. diff --git a/documentation/audits/day-2026-10-08/r232/README.md b/documentation/audits/day-2026-10-08/r232/README.md new file mode 100644 index 00000000..aee7ae78 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r232/README.md @@ -0,0 +1,30 @@ +# R-232 (a) — DooPlex's backup mails the operator when it fails (2026-10-08) + +**Operator word:** "Yes" in chat (2026-10-08, asked: "May I change DooPlex's backup notification so a failed run sends a +mail? One setting, plus one test mail. I will not start a backup run."). + +**What changed (DooPlex, unversioned scripts — R-231):** +- `/opt/backup/scripts/backup-config.sh`: `notify_failure` now also sends a mail through Resend (the API the CI failure + mail uses) from `monitoring@felhom.eu` to `admin@felhom.eu`. The webhook branch is unchanged. The mail never changes a + backup's exit code (`return 0`) and logs its outcome to `backup.log`. Before/after: `backup-config.sh.before`, + `backup-config.sh.after` (no secret in either). The old file is also kept beside it as + `backup-config.sh.bak-20261008-080524`. +- `/etc/backup/resend-api-key`: new, `600 root`, 36 bytes, copied from the k3s Secret `felhom-system/resend-api` with + `umask 077` and never printed. +- Nothing else in DooPlex's backup changed. **No backup run was started.** + +**Proof (two channels):** +1. The function's own output (`test-mail.txt`): `sudo bash -c 'source …/backup-config.sh; notify_failure "TEST - R-232 + wiring check, no backup ran"'` → `notify_failure: mail accepted id=01a11a1d-…`, `rc=0`, and the line + `[INFO] notify_failure: failure mail sent to admin@felhom.eu` in `backup.log`. +2. The inbox (Gmail connector, which reads the admin@ catch-all): one message, 2026-10-08T06:05:25Z, from + `monitoring@felhom.eu`, subject `[DooPlex backup] FAILED: TEST - R-232 wiring check, no backup ran`, label INBOX. + +**Not proven:** a real failure path end to end (no backup was forced to fail, by the brief). The callers are the +existing `ERR` traps and `backup-all.sh`'s component check, unchanged. + +**Rollback:** `sudo cp -p /opt/backup/scripts/backup-config.sh.bak-20261008-080524 /opt/backup/scripts/backup-config.sh` +and `sudo rm /etc/backup/resend-api-key`. + +**If the Resend key is rotated:** this file must be refreshed too (a second consumer of `Secret/resend-api`, beside the +hub and contact-mailer). diff --git a/documentation/audits/day-2026-10-08/r232/backup-config.sh.after b/documentation/audits/day-2026-10-08/r232/backup-config.sh.after new file mode 100755 index 00000000..80619630 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r232/backup-config.sh.after @@ -0,0 +1,178 @@ +#!/bin/bash +# Dooplex Cluster Backup Configuration +# Source this file in backup scripts: source /opt/backup/backup-config.sh + +# ============================================================================ +# BACKUP DESTINATIONS +# ============================================================================ +export BACKUP_BASE="/mnt/5_hdd/backup" +export BACKUP_K3S="${BACKUP_BASE}/k3s" +export BACKUP_SECRETS="${BACKUP_BASE}/secrets" +export BACKUP_MANIFESTS="${BACKUP_BASE}/homelab-manifests" +# NOT under BACKUP_BASE. DATA_SOURCE_DIR moved to /mnt/5_hdd/data in the +# 2026-08-14 migration off the failed 4_hdd, so leaving this repo under +# BACKUP_BASE (also 5_hdd) would put the backup on the same physical disk as +# its source -- protection against accidental deletion, none against loss of +# sda1. 1_hdd holds no Longhorn replicas and only serves Plex reads, so backup +# writes do not contend with live volume I/O. +export BACKUP_DATA="/mnt/1_hdd/backup/data" +export BACKUP_LONGHORN="${BACKUP_BASE}/longhorn-pvc" +export BACKUP_LOGS="${BACKUP_BASE}/logs" + +# ============================================================================ +# RESTIC REPOSITORIES (each category has its own repo for flexibility) +# ============================================================================ +export RESTIC_REPO_K3S="${BACKUP_K3S}/restic-repo" +export RESTIC_REPO_SECRETS="${BACKUP_SECRETS}/restic-repo" +export RESTIC_REPO_DATA="${BACKUP_DATA}/restic-repo" +export BACKUP_POSTGRESQL="${BACKUP_BASE}/postgresql" +export POSTGRESQL_DUMP_DIR="${BACKUP_POSTGRESQL}/dumps" +export RESTIC_REPO_POSTGRESQL="${BACKUP_POSTGRESQL}/restic-repo" + +# ============================================================================ +# RESTIC PASSWORD (change this!) +# Store in /etc/backup/restic-password or set RESTIC_PASSWORD_FILE +# ============================================================================ +export RESTIC_PASSWORD_FILE="/etc/backup/restic-password" + +# ============================================================================ +# RETENTION POLICY +# ============================================================================ +export RETENTION_KEEP_LAST=7 +export RETENTION_KEEP_DAILY=7 +export RETENTION_KEEP_WEEKLY=4 +export RETENTION_KEEP_MONTHLY=6 + +# ============================================================================ +# SOURCE DIRECTORIES +# ============================================================================ +export K3S_SERVER_DIR="/var/lib/rancher/k3s/server" +export K3S_CONFIG_DIR="/etc/rancher/k3s" +export DATA_SOURCE_DIR="/mnt/5_hdd/data" + +# Claude Code auto-memory store (R-229, 2026-08-06). Rides in the User Data component because it is +# small, exists on this host only, and is in NO git repository -- /mnt/5_hdd/felhom.eu/git is not a +# repo, so nothing else preserves it. BACKED UP, NOT COMMITTED: it is auto-written and may name +# hosts and paths that the project's secrets rule keeps out of committed files. +# CAVEAT: BACKUP_BASE is on the SAME physical disk (/mnt/5_hdd) as this source, so this protects +# against accidental deletion, NOT against loss of sda1. +export CLAUDE_MEMORY_DIR="/mnt/5_hdd/felhom.eu/git/.claude-memory" + +# ============================================================================ +# EXCLUDES +# ============================================================================ +export DATA_EXCLUDES=( + "*.tmp" + "*.temp" + "*.cache" + "**/cache/**" + "**/Cache/**" + "**/.cache/**" + "**/node_modules/**" + "**/__pycache__/**" + "**/Thumbs.db" + "**/.DS_Store" +) + +# ============================================================================ +# NOTIFICATION (optional - configure as needed) +# ============================================================================ +export NOTIFY_ON_FAILURE="true" +# export NOTIFY_WEBHOOK_URL="https://your-webhook-url" +# R-232 (a), 2026-10-08 (operator yes in chat): a failed run is MAILED through the project's existing mail path +# (Resend, the same API the CI failure mail uses) to the operator. The key is stored out-of-band, root-only, in +# NOTIFY_RESEND_KEY_FILE (copied from the k3s Secret felhom-system/resend-api); it is never printed. +export NOTIFY_RESEND_KEY_FILE="/etc/backup/resend-api-key" +export NOTIFY_MAIL_FROM="DooPlex backup " +export NOTIFY_MAIL_TO="admin@felhom.eu" + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +log() { + local level="$1" + shift + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "${BACKUP_LOGS}/backup.log" +} + +log_info() { log "INFO" "$@"; } +log_warn() { log "WARN" "$@"; } +log_error() { log "ERROR" "$@"; } + +check_restic() { + if ! command -v restic &> /dev/null; then + log_error "restic is not installed. Install with: apt install restic" + exit 1 + fi +} + +check_kubectl() { + if ! command -v kubectl &> /dev/null; then + log_error "kubectl is not installed" + exit 1 + fi +} + +ensure_dirs() { + mkdir -p "${BACKUP_K3S}" "${BACKUP_SECRETS}" "${BACKUP_MANIFESTS}" \ + "${BACKUP_DATA}" "${BACKUP_LONGHORN}" "${BACKUP_LOGS}" "${BACKUP_POSTGRESQL}" +} + +init_restic_repo() { + local repo="$1" + if [ ! -d "${repo}" ]; then + log_info "Initializing restic repository: ${repo}" + restic -r "${repo}" init + fi +} + +apply_retention() { + local repo="$1" + log_info "Applying retention policy to ${repo}" + restic -r "${repo}" forget \ + --keep-last ${RETENTION_KEEP_LAST} \ + --keep-daily ${RETENTION_KEEP_DAILY} \ + --keep-weekly ${RETENTION_KEEP_WEEKLY} \ + --keep-monthly ${RETENTION_KEEP_MONTHLY} \ + --prune +} + +notify_failure() { + local message="$1" + if [ "${NOTIFY_ON_FAILURE}" = "true" ] && [ -n "${NOTIFY_WEBHOOK_URL}" ]; then + curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"text\": \"🚨 Backup Failed: ${message}\"}" \ + "${NOTIFY_WEBHOOK_URL}" || true + fi + # R-232 (a): the mail. Never fails the caller (a broken mail must not change a backup's exit code); logs its outcome. + if [ "${NOTIFY_ON_FAILURE}" = "true" ] && [ -r "${NOTIFY_RESEND_KEY_FILE}" ]; then + if NOTIFY_MSG="${message}" NOTIFY_HOST="$(hostname)" NOTIFY_LOG="${BACKUP_LOGS}/backup.log" python3 - <<'PY' +import json, os, sys, urllib.error, urllib.request +key = open(os.environ["NOTIFY_RESEND_KEY_FILE"]).read().strip() +msg, host = os.environ.get("NOTIFY_MSG", ""), os.environ.get("NOTIFY_HOST", "?") +body = json.dumps({ + "from": os.environ["NOTIFY_MAIL_FROM"], "to": [os.environ["NOTIFY_MAIL_TO"]], + "subject": "[DooPlex backup] FAILED: %s" % msg, + "text": "DooPlex's backup reported a failure.\n\nHost : %s\nFailure: %s\nLog : %s\n\n" + "See journalctl -u dooplex-backup and the log above. This mail is sent by notify_failure " + "in /opt/backup/scripts/backup-config.sh (R-232 a).\n" % (host, msg, os.environ.get("NOTIFY_LOG", "")), +}).encode() +req = urllib.request.Request("https://api.resend.com/emails", data=body, method="POST", + headers={"Authorization": "Bearer %s" % key, "Content-Type": "application/json", + # Cloudflare fronts api.resend.com and blocks the default Python-urllib agent (error 1010). + "User-Agent": "dooplex-backup/1.0"}) +try: + with urllib.request.urlopen(req, timeout=30) as r: + print("notify_failure: mail accepted id=%s" % json.load(r).get("id")) +except urllib.error.HTTPError as e: + sys.exit("notify_failure: Resend HTTP %s" % e.code) +except Exception as e: + sys.exit("notify_failure: mail not sent (%s)" % type(e).__name__) +PY + then log_info "notify_failure: failure mail sent to ${NOTIFY_MAIL_TO}" + else log_error "notify_failure: the failure mail could NOT be sent" + fi + fi + return 0 +} diff --git a/documentation/audits/day-2026-10-08/r232/backup-config.sh.before b/documentation/audits/day-2026-10-08/r232/backup-config.sh.before new file mode 100755 index 00000000..c188f674 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r232/backup-config.sh.before @@ -0,0 +1,142 @@ +#!/bin/bash +# Dooplex Cluster Backup Configuration +# Source this file in backup scripts: source /opt/backup/backup-config.sh + +# ============================================================================ +# BACKUP DESTINATIONS +# ============================================================================ +export BACKUP_BASE="/mnt/5_hdd/backup" +export BACKUP_K3S="${BACKUP_BASE}/k3s" +export BACKUP_SECRETS="${BACKUP_BASE}/secrets" +export BACKUP_MANIFESTS="${BACKUP_BASE}/homelab-manifests" +# NOT under BACKUP_BASE. DATA_SOURCE_DIR moved to /mnt/5_hdd/data in the +# 2026-08-14 migration off the failed 4_hdd, so leaving this repo under +# BACKUP_BASE (also 5_hdd) would put the backup on the same physical disk as +# its source -- protection against accidental deletion, none against loss of +# sda1. 1_hdd holds no Longhorn replicas and only serves Plex reads, so backup +# writes do not contend with live volume I/O. +export BACKUP_DATA="/mnt/1_hdd/backup/data" +export BACKUP_LONGHORN="${BACKUP_BASE}/longhorn-pvc" +export BACKUP_LOGS="${BACKUP_BASE}/logs" + +# ============================================================================ +# RESTIC REPOSITORIES (each category has its own repo for flexibility) +# ============================================================================ +export RESTIC_REPO_K3S="${BACKUP_K3S}/restic-repo" +export RESTIC_REPO_SECRETS="${BACKUP_SECRETS}/restic-repo" +export RESTIC_REPO_DATA="${BACKUP_DATA}/restic-repo" +export BACKUP_POSTGRESQL="${BACKUP_BASE}/postgresql" +export POSTGRESQL_DUMP_DIR="${BACKUP_POSTGRESQL}/dumps" +export RESTIC_REPO_POSTGRESQL="${BACKUP_POSTGRESQL}/restic-repo" + +# ============================================================================ +# RESTIC PASSWORD (change this!) +# Store in /etc/backup/restic-password or set RESTIC_PASSWORD_FILE +# ============================================================================ +export RESTIC_PASSWORD_FILE="/etc/backup/restic-password" + +# ============================================================================ +# RETENTION POLICY +# ============================================================================ +export RETENTION_KEEP_LAST=7 +export RETENTION_KEEP_DAILY=7 +export RETENTION_KEEP_WEEKLY=4 +export RETENTION_KEEP_MONTHLY=6 + +# ============================================================================ +# SOURCE DIRECTORIES +# ============================================================================ +export K3S_SERVER_DIR="/var/lib/rancher/k3s/server" +export K3S_CONFIG_DIR="/etc/rancher/k3s" +export DATA_SOURCE_DIR="/mnt/5_hdd/data" + +# Claude Code auto-memory store (R-229, 2026-08-06). Rides in the User Data component because it is +# small, exists on this host only, and is in NO git repository -- /mnt/5_hdd/felhom.eu/git is not a +# repo, so nothing else preserves it. BACKED UP, NOT COMMITTED: it is auto-written and may name +# hosts and paths that the project's secrets rule keeps out of committed files. +# CAVEAT: BACKUP_BASE is on the SAME physical disk (/mnt/5_hdd) as this source, so this protects +# against accidental deletion, NOT against loss of sda1. +export CLAUDE_MEMORY_DIR="/mnt/5_hdd/felhom.eu/git/.claude-memory" + +# ============================================================================ +# EXCLUDES +# ============================================================================ +export DATA_EXCLUDES=( + "*.tmp" + "*.temp" + "*.cache" + "**/cache/**" + "**/Cache/**" + "**/.cache/**" + "**/node_modules/**" + "**/__pycache__/**" + "**/Thumbs.db" + "**/.DS_Store" +) + +# ============================================================================ +# NOTIFICATION (optional - configure as needed) +# ============================================================================ +export NOTIFY_ON_FAILURE="true" +# export NOTIFY_WEBHOOK_URL="https://your-webhook-url" + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +log() { + local level="$1" + shift + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "${BACKUP_LOGS}/backup.log" +} + +log_info() { log "INFO" "$@"; } +log_warn() { log "WARN" "$@"; } +log_error() { log "ERROR" "$@"; } + +check_restic() { + if ! command -v restic &> /dev/null; then + log_error "restic is not installed. Install with: apt install restic" + exit 1 + fi +} + +check_kubectl() { + if ! command -v kubectl &> /dev/null; then + log_error "kubectl is not installed" + exit 1 + fi +} + +ensure_dirs() { + mkdir -p "${BACKUP_K3S}" "${BACKUP_SECRETS}" "${BACKUP_MANIFESTS}" \ + "${BACKUP_DATA}" "${BACKUP_LONGHORN}" "${BACKUP_LOGS}" "${BACKUP_POSTGRESQL}" +} + +init_restic_repo() { + local repo="$1" + if [ ! -d "${repo}" ]; then + log_info "Initializing restic repository: ${repo}" + restic -r "${repo}" init + fi +} + +apply_retention() { + local repo="$1" + log_info "Applying retention policy to ${repo}" + restic -r "${repo}" forget \ + --keep-last ${RETENTION_KEEP_LAST} \ + --keep-daily ${RETENTION_KEEP_DAILY} \ + --keep-weekly ${RETENTION_KEEP_WEEKLY} \ + --keep-monthly ${RETENTION_KEEP_MONTHLY} \ + --prune +} + +notify_failure() { + local message="$1" + if [ "${NOTIFY_ON_FAILURE}" = "true" ] && [ -n "${NOTIFY_WEBHOOK_URL}" ]; then + curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"text\": \"🚨 Backup Failed: ${message}\"}" \ + "${NOTIFY_WEBHOOK_URL}" || true + fi +} diff --git a/documentation/audits/day-2026-10-08/r232/test-mail.txt b/documentation/audits/day-2026-10-08/r232/test-mail.txt new file mode 100644 index 00000000..7486ff46 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r232/test-mail.txt @@ -0,0 +1,5 @@ +notify_failure: mail accepted id=01a11a1d-d554-7f1f-acb7-ba5c268b70a6 +[2026-10-08 08:05:25] [INFO] notify_failure: failure mail sent to admin@felhom.eu +rc=0 +[2026-10-08 03:14:36] [INFO] ======================================================== +[2026-10-08 08:05:25] [INFO] notify_failure: failure mail sent to admin@felhom.eu diff --git a/documentation/audits/day-2026-10-08/r762/README.md b/documentation/audits/day-2026-10-08/r762/README.md new file mode 100644 index 00000000..e93d9613 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/README.md @@ -0,0 +1,69 @@ +# R-762 (wger's real web server: gunicorn with 2 workers): 2026-10-08 day, Part D + +**Status: the bench half is done. The 9202 half is NOT done, so nothing was committed to the catalog.** +To reach 9202, the drill catalog had to be brought up to date and wger un-hidden in it (wger is `lifecycle: hidden`, +and the controller refuses to deploy a hidden app, `router.go` L461). The permission check refused that write +(„Modify Shared Resources"). The brief says a refusal stops the item, so it stopped there. The drill repo, 9202's +catalog setting and the live catalog were not changed. `app-catalog-felhom.eu` is clean at `32d1346`. + +## The definition tested +`definition-docker-compose.yml` is the current `templates/wger/docker-compose.yml` with two more env lines and a comment: +`WGER_USE_GUNICORN=True` and `WEB_CONCURRENCY=2`. + +Checked in the image `wger/server:2.7` (`sha256:1c5789b9…`, the same digest the ladder records) on bench 9401: +- `/home/wger/entrypoint.sh` runs `gunicorn wger.wsgi:application --preload --bind 0.0.0.0:$PORT` only when + `WGER_USE_GUNICORN == "True"`. Otherwise it runs `manage.py runserver`. +- There is no `-w`, no `gunicorn.conf.py` in the working directory `/home/wger/src`, and no GUNICORN or WEB_ env in the + image. +- gunicorn 26.1.0's own `Config()` gives `workers` 1 by default and 2 with `WEB_CONCURRENCY=2`. The default timeout is + 30 s. + +## Bench 9401 (demo-hp), two runs +1. **`upgrade-test.py --soak 600 --move-to wger wger@gunicorn`** (harness v5): FROM the template as it stands (runserver) + TO the gunicorn definition. Raw output: `bench/R762-gunicorn.log` and `bench/evidence/MV-wger/`. + - The verdict is `proven`. The seed was read back before and after the switch. The app was healthy after it. The + abort was `starts-and-serves`. Measured at 2026-10-08T06:19:56Z. + - The memory watch ran for 606.5 s with 11,584 requests (all 302) and `load: reached`. + - wger: anon peak 178,151,424 B = 170 MiB = **44.2 %** of 384M; 0 oom_kills; 0 restarts; the cgroup peak was + 100 % (page cache). + - wger-files: anon peak 15.8 %; 0 kills; 0 restarts. + - `to-full.log` has „Using gunicorn on port 8000..." and **2 × „Booting worker"** (pids 18 and 19). +2. **`check/benchcheck.sh`**: the definition started fresh in project `r762b`, then the login page, its CSS, a photo + and the workers were read. Results in `check/`. + - Ready after 77 s. **Login page 200.** + - **CSS 200** for all 3 of the page's own links, through wger-files (`text/css`; 2481 B, 277042 B and 1006 B). + - Web login 302 with a session. `POST /api/v2/gallery/` with a 179 B PNG returned 201. **The photo read back through + wger-files: 200, 179 B, `image/png`.** + - Control: an unknown `/static/` file returned 404. + - The container's env holds `WGER_USE_GUNICORN=True` and `WEB_CONCURRENCY=2`. Its log has **2 × „Booting worker"**. + - anon peak 174,047,232 B = 166 MiB = 43.2 %. oom 0, oom_kill 0. restarts=0, oomkilled=false. + - 20 parallel login GETs all returned 200. + - This run is short. The 10-minute watch is run 1. + +The night's figure (`night-burndown-2026-10-06/r762/`) was 157 MiB, 41 %, on the template without traefik's env. Today +it is 166 to 170 MiB, 43 to 44 %, on the full catalog template. + +## The ladder: why no step file was written +The ladder records image moves. This change moves no image: `from` and `to` would both be +`{wger: wger/server:2.7, wger-files: nginx:1.30.5-alpine}` (step key `10df849ded803b6e`). The writer handles +from == to as a re-test, and `ladder.check_entry` refuses that entry: +„a re-test (from == to) whose digest is the same as its digest_from tests nothing new — no new digest" (tool output, +2026-10-08). `09` §5.4's render table says „deployed, pinned, catalog images equal → the catalog template — fixes flow". +So a compose-only change reaches an installed wger at its next `up -d`, and the product's restart is `up -d` +(`manager.go` ~L1411). It needs no ladder entry. No box runs wger (hub read, 07:58). + +## Teardown +- **Machine (bench 9401):** project `r762b` was taken down with `down -v`; afterwards 0 containers and 0 `r762` + volumes. The harness ran its own `down -v`. `/root/r762b` and the helper scripts were deleted. `/opt/upg/templates/wger@gunicorn` + was deleted. `/opt/upg`'s scripts and `templates/wger` were updated to the catalog's `32d1346` copies; they were + older before. `/opt/upg/evidence/MV-wger` now holds today's run (the 10-06 run is kept in + `audits/design-build-2026-10-06/F/bench/`). `/opt/upg/R762-gunicorn.log` stays. **9401 was stopped** (`pct status`: + stopped), as it was found. +- **Machine (9202):** only GETs and a dashboard login. 0 wger containers; `repo_url` is still the live catalog. Its + deployed list was paperless-ngx, privatebin and recipe-importer at 08:0x CEST and paperless-ngx and privatebin at + 08:35. **This session did not touch recipe-importer**; something else removed it in that window. +- **Host (demo-hp):** the `/tmp` copy files were deleted. Nothing else was created. +- **Hub:** nothing. +- No Docker command ran on DooPlex. Nothing was pruned. + +The image's default admin password is redacted in every file here. diff --git a/documentation/audits/day-2026-10-08/r762/bench/R762-gunicorn.log b/documentation/audits/day-2026-10-08/r762/bench/R762-gunicorn.log new file mode 100644 index 00000000..43550808 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/R762-gunicorn.log @@ -0,0 +1,130 @@ +[06:05:43] scratch drive folders cleared before FROM (R-656): none existed +[06:05:43] MV-wger: deploying wger at FROM {'wger': 'wger/server:2.7', 'wger-files': 'nginx:1.30.5-alpine'} +[06:07:17] FROM settled=True in 93.0s :: {"wger": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}, "wger-files": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[06:07:17] fixture: the BOX walk's own (Wger), through upgrade_boxport +[06:07:17] wger: the generated admin password does not log in (POST /en/user/login -> 200) — running the template's own after_install command (the app's CLI, as the product does after an install) +[06:07:19] wger: after_install :: version 8.3.1, blocking by username FELHOM_AFTER_INSTALL_OK +[06:07:21] wger: POST /api/v2/weightentry/ http=201 +[06:07:21] wger: readback of the seeded weight entry http=200 found=True +[06:07:21] C1 (seed reads back BEFORE): True +[06:07:21] MV-wger: swapping to TO {'wger': 'wger/server:2.7', 'wger-files': 'nginx:1.30.5-alpine'} +[06:07:33] TO up -d rc=0 +[06:08:35] TO settled=True in 62.1s :: {"wger": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}, "wger-files": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[06:08:35] migration lines observed: 6 +[06:08:35] wger: readback of the seeded weight entry http=200 found=True +[06:08:35] RESULT (seed reads back AFTER): True +[06:08:36] memory watch: 600s, 4 callers on 1 path(s) at 172.18.0.2:8000 +[06:08:51] + 15s wger=288M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=292 +[06:09:06] + 30s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=580 +[06:09:21] + 46s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=872 +[06:09:37] + 61s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1160 +[06:09:52] + 76s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1452 +[06:10:07] + 91s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1740 +[06:10:22] + 106s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2028 +[06:10:37] + 121s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2320 +[06:10:52] + 136s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2608 +[06:11:07] + 152s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2896 +[06:11:23] + 167s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3188 +[06:11:38] + 182s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3476 +[06:11:53] + 197s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3768 +[06:12:08] + 212s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4056 +[06:12:23] + 227s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4344 +[06:12:38] + 242s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4636 +[06:12:54] + 258s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4924 +[06:13:09] + 273s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5216 +[06:13:24] + 288s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5504 +[06:13:39] + 303s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5792 +[06:13:54] + 318s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6084 +[06:14:09] + 334s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6372 +[06:14:25] + 349s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6664 +[06:14:40] + 364s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6952 +[06:14:55] + 379s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7240 +[06:15:10] + 394s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7529 +[06:15:25] + 409s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7820 +[06:15:40] + 424s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8108 +[06:15:55] + 440s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8400 +[06:16:11] + 455s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8688 +[06:16:26] + 470s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8978 +[06:16:41] + 485s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9268 +[06:16:56] + 500s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9556 +[06:17:11] + 515s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9848 +[06:17:26] + 530s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10136 +[06:17:42] + 546s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10428 +[06:17:57] + 561s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10716 +[06:18:12] + 576s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11008 +[06:18:27] + 591s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11296 +[06:18:42] + 606s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11584 +[06:18:42] memory watch: killed=False tight=[] requests=11584 codes={'302': 11584} +[06:18:42] MV-wger: ABORT — putting the FROM images back +[06:19:56] wger: readback of the seeded weight entry http=200 found=True +[06:19:56] ABORT: app came back in 62.0s; data present=True +{ + "harness_version": 5, + "edge": "MV-wger", + "app": "wger", + "note": "definition step to wger@gunicorn", + "from": { + "wger": "wger/server:2.7", + "wger-files": "nginx:1.30.5-alpine" + }, + "to": { + "wger": "wger/server:2.7", + "wger-files": "nginx:1.30.5-alpine" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": "\u001b[2Kwger | Performing database migrations", + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 606.5, + "requested_s": 600, + "requests": 11584, + "codes": { + "302": 11584 + }, + "first_kill": null, + "containers": { + "wger": { + "limit": 402653184, + "peak": 402653184, + "peak_pct": 1.0, + "anon_peak_sampled": 178151424, + "anon_peak_pct": 0.442, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + }, + "wger-files": { + "limit": 33554432, + "peak": 9281536, + "peak_pct": 0.277, + "anon_peak_sampled": 5308416, + "anon_peak_pct": 0.158, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 62.1, + "measured_at": "2026-10-08T06:19:56Z", + "evidence": "evidence/MV-wger", + "scratch_cleared": [], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 853.6 +} diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/anon-max b/documentation/audits/day-2026-10-08/r762/bench/check/anon-max new file mode 100644 index 00000000..e9b4aca9 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/anon-max @@ -0,0 +1 @@ +174047232 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/benchcheck.sh b/documentation/audits/day-2026-10-08/r762/bench/check/benchcheck.sh new file mode 100755 index 00000000..d0ceda48 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/benchcheck.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# R-762 bench check (2026-10-08): the gunicorn definition up on its own, then the login page, its CSS, a photo read back, +# the workers in the container's own log, and the app's memory (anon). Project r762b only; down -v at the end. +set -u +W=/root/r762b; O=$W/out; rm -rf $W; mkdir -p $O; cd $W +cp /opt/upg/templates/wger@gunicorn/docker-compose.yml docker-compose.yml +umask 077 +printf 'DOMAIN=bench.invalid\nSUBDOMAIN=fitness\nSECRET_KEY=%s\n' "$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" > .env +docker compose -p r762b up -d > $O/up.txt 2>&1 +ID=$(docker inspect -f '{{.Id}}' wger); CG=$(find /sys/fs/cgroup -maxdepth 6 -type d -name "*$ID*" | head -1) +touch $W/.sampling; ( max=0; while [ -f $W/.sampling ]; do a=$(awk '$1=="anon"{print $2}' $CG/memory.stat 2>/dev/null); [ -n "$a" ] && [ "$a" -gt "$max" ] && max=$a && echo $max > $O/anon-max; sleep 0.5; done ) & +echo "cgroup: $CG" > $O/cg.txt +IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' wger) +FIP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' wger-files) +t0=$(date +%s); for i in $(seq 1 300); do c=$(curl -s -o /dev/null -w '%{http_code}' http://$IP:8000/en/user/login); [ "$c" = 200 ] && break; sleep 1; done +echo "ready_after_s=$(( $(date +%s)-t0 )) login=$c" > $O/checks.txt +curl -s http://$IP:8000/en/user/login | grep -o '/static/[^"]*\.css' | head -3 | while read l; do echo "css $l via wger-files: $(curl -s -o /dev/null -w '%{http_code} %{size_download}B %{content_type}' http://$FIP$l)"; done >> $O/checks.txt +# a photo through wger's own gallery API, signed in through its own login form as the image's seeded admin (bench-only +# throwaway box; the password is not written anywhere). As a browser behind traefik: Host + X-Forwarded-Proto https, the +# cookies carried by hand (Django's csrftoken is Secure; curl drops it over http). +H=(-H "Host: fitness.bench.invalid" -H "X-Forwarded-Proto: https" -H "Origin: https://fitness.bench.invalid" -H "Referer: https://fitness.bench.invalid/en/user/login") +curl -s -D $W/h1 -o $W/b1 "${H[@]}" http://$IP:8000/en/user/login +CSRFC=$(grep -i '^set-cookie: csrftoken=' $W/h1 | sed 's/^[^=]*=//; s/;.*//' | tr -d '\r') +FORM=$(grep -o 'name="csrfmiddlewaretoken" value="[^"]*"' $W/b1 | head -1 | sed 's/.*value="//; s/"$//') +curl -s -D $W/h2 -o /dev/null "${H[@]}" -H "Cookie: csrftoken=$CSRFC" --data-urlencode "csrfmiddlewaretoken=$FORM" --data-urlencode login=admin --data-urlencode password= http://$IP:8000/en/user/login +SID=$(grep -i '^set-cookie: sessionid=' $W/h2 | sed 's/^[^=]*=//; s/;.*//' | tr -d '\r') +CSRF2=$(grep -i '^set-cookie: csrftoken=' $W/h2 | sed 's/^[^=]*=//; s/;.*//' | tr -d '\r'); [ -n "$CSRF2" ] && CSRFC=$CSRF2 +echo "web login: $(head -1 $W/h2 | tr -d '\r'), session cookie: $([ -n "$SID" ] && echo yes || echo no)" >> $O/checks.txt +rm -f $W/h1 $W/h2 $W/b1 +python3 - > $W/p.png <<'PY' +import struct, zlib, sys, os +n=64; rgb=os.urandom(3); raw=b"".join(b"\x00"+rgb*n for _ in range(n)) +def ch(t,d): return struct.pack(">I",len(d))+t+d+struct.pack(">I",zlib.crc32(t+d)&0xffffffff) +sys.stdout.buffer.write(b"\x89PNG\r\n\x1a\n"+ch(b"IHDR",struct.pack(">IIBBBBB",n,n,8,2,0,0,0))+ch(b"IDAT",zlib.compress(raw))+ch(b"IEND",b"")) +PY +SZ=$(stat -c %s $W/p.png) +R=$(curl -s -w '\n%{http_code}' "${H[@]}" -H "Cookie: csrftoken=$CSRFC; sessionid=$SID" -H "X-CSRFToken: $CSRFC" -F "image=@$W/p.png;type=image/png" -F date=2026-10-08 -F description=r762 http://$IP:8000/api/v2/gallery/) +echo "POST /api/v2/gallery/ (${SZ} B PNG) -> $(echo "$R" | tail -1)" >> $O/checks.txt +P=$(echo "$R" | head -n -1 | python3 -c 'import json,sys,re; print(re.sub(r"^https?://[^/]+","",json.load(sys.stdin).get("image","")))' 2>/dev/null) +[ -z "$P" ] && echo "photo: NO PATH returned (the upload failed)" >> $O/checks.txt +[ -n "$P" ] && echo "photo $P via wger-files: $(curl -s -o /dev/null -w '%{http_code} %{size_download}B %{content_type}' http://$FIP$P)" >> $O/checks.txt +echo "control: an unknown /static/ file via wger-files: $(curl -s -o /dev/null -w '%{http_code}' http://$FIP/static/r762-nonexistent.css)" >> $O/checks.txt +# light load: 20 login GETs, 10 parallel +seq 1 20 | xargs -P 10 -I{} curl -s -o /dev/null -w 'conc %{http_code} %{time_total}\n' http://$IP:8000/en/user/login > $O/conc.txt +sleep 3; rm -f $W/.sampling; sleep 1 +grep -E '^(oom|oom_kill) ' $CG/memory.events > $O/events.txt; cat $CG/memory.max > $O/memory.max; cat $CG/memory.peak > $O/memory.peak 2>/dev/null +docker inspect -f 'restarts={{.RestartCount}} oomkilled={{.State.OOMKilled}} status={{.State.Status}} health={{.State.Health.Status}}' wger > $O/inspect.txt +docker exec wger sh -c 'env | grep -E "^(WGER_USE_GUNICORN|WEB_CONCURRENCY)="' > $O/env-in-container.txt +docker logs wger 2>&1 | sed 's///g' > $O/wger.log +grep -E 'Using gunicorn|Using django|Booting worker|Listening at|Starting gunicorn|Starting development server' $O/wger.log > $O/server-lines.txt +docker compose -p r762b down -v > $O/down.txt 2>&1 +rm -f $W/p.png .env +echo done diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/cg.txt b/documentation/audits/day-2026-10-08/r762/bench/check/cg.txt new file mode 100644 index 00000000..d66300b8 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/cg.txt @@ -0,0 +1 @@ +cgroup: /sys/fs/cgroup/system.slice/docker-1dd3339a4eeb9ed63540b35d7ca09acca030259a68038e233e7270947dadcb05.scope diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/checks.txt b/documentation/audits/day-2026-10-08/r762/bench/check/checks.txt new file mode 100644 index 00000000..120cfc14 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/checks.txt @@ -0,0 +1,8 @@ +ready_after_s=77 login=200 +css /static/css/workout-manager.7007d84ce531.css via wger-files: 200 2481B text/css +css /static/bootstrap-compiled.80a6279921f8.css via wger-files: 200 277042B text/css +css /static/css/bootstrap-custom.400ad578123c.css via wger-files: 200 1006B text/css +web login: HTTP/1.1 302 Found, session cookie: yes +POST /api/v2/gallery/ (179 B PNG) -> 201 +photo /media/gallery/1/badf61de-7554-44b9-b72f-87df80fbd01d.png via wger-files: 200 179B image/png +control: an unknown /static/ file via wger-files: 404 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/conc.txt b/documentation/audits/day-2026-10-08/r762/bench/check/conc.txt new file mode 100644 index 00000000..8aeb42b5 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/conc.txt @@ -0,0 +1,20 @@ +conc 200 0.044549 +conc 200 0.080295 +conc 200 0.119458 +conc 200 0.140526 +conc 200 0.147622 +conc 200 0.169720 +conc 200 0.175291 +conc 200 0.200793 +conc 200 0.203844 +conc 200 0.221900 +conc 200 0.196650 +conc 200 0.178437 +conc 200 0.147859 +conc 200 0.152214 +conc 200 0.146952 +conc 200 0.140927 +conc 200 0.146328 +conc 200 0.139658 +conc 200 0.140157 +conc 200 0.139606 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/down.txt b/documentation/audits/day-2026-10-08/r762/bench/check/down.txt new file mode 100644 index 00000000..d9a73562 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/down.txt @@ -0,0 +1,14 @@ + Container wger-files Stopping + Container wger-files Stopped + Container wger-files Removing + Container wger-files Removed + Container wger Stopping + Container wger Stopped + Container wger Removing + Container wger Removed + Volume r762b_wger_media Removing + Volume r762b_wger_data Removing + Volume r762b_wger_static Removing + Volume r762b_wger_media Removed + Volume r762b_wger_static Removed + Volume r762b_wger_data Removed diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/env-in-container.txt b/documentation/audits/day-2026-10-08/r762/bench/check/env-in-container.txt new file mode 100644 index 00000000..1f9a15fd --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/env-in-container.txt @@ -0,0 +1,2 @@ +WGER_USE_GUNICORN=True +WEB_CONCURRENCY=2 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/events.txt b/documentation/audits/day-2026-10-08/r762/bench/check/events.txt new file mode 100644 index 00000000..a0829c28 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/events.txt @@ -0,0 +1,2 @@ +oom 0 +oom_kill 0 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/inspect.txt b/documentation/audits/day-2026-10-08/r762/bench/check/inspect.txt new file mode 100644 index 00000000..c55ebe36 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/inspect.txt @@ -0,0 +1 @@ +restarts=0 oomkilled=false status=running health=starting diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/memory.max b/documentation/audits/day-2026-10-08/r762/bench/check/memory.max new file mode 100644 index 00000000..4854b40a --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/memory.max @@ -0,0 +1 @@ +402653184 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/memory.peak b/documentation/audits/day-2026-10-08/r762/bench/check/memory.peak new file mode 100644 index 00000000..4854b40a --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/memory.peak @@ -0,0 +1 @@ +402653184 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/server-lines.txt b/documentation/audits/day-2026-10-08/r762/bench/check/server-lines.txt new file mode 100644 index 00000000..14704be1 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/server-lines.txt @@ -0,0 +1,5 @@ +Using gunicorn on port 8000... +[2026-10-08 08:32:13 +0200] [28] [INFO] Starting gunicorn 26.1.0 +[2026-10-08 08:32:13 +0200] [28] [INFO] Listening at: http://0.0.0.0:8000 (28) +[2026-10-08 08:32:13 +0200] [29] [INFO] Booting worker with pid: 29 +[2026-10-08 08:32:13 +0200] [30] [INFO] Booting worker with pid: 30 diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/up.txt b/documentation/audits/day-2026-10-08/r762/bench/check/up.txt new file mode 100644 index 00000000..fd2a97bc --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/up.txt @@ -0,0 +1,14 @@ + Volume "r762b_wger_static" Creating + Volume "r762b_wger_static" Created + Volume "r762b_wger_data" Creating + Volume "r762b_wger_data" Created + Volume "r762b_wger_media" Creating + Volume "r762b_wger_media" Created + Container wger Creating + Container wger Created + Container wger-files Creating + Container wger-files Created + Container wger Starting + Container wger Started + Container wger-files Starting + Container wger-files Started diff --git a/documentation/audits/day-2026-10-08/r762/bench/check/wger.log b/documentation/audits/day-2026-10-08/r762/bench/check/wger.log new file mode 100644 index 00000000..eaaa3cd9 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/check/wger.log @@ -0,0 +1,295 @@ +*** Using settings from env: settings.main +level=INFO ts=2026-10-08 08:31:01,000 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 +*** Database is empty or incomplete, setting it up now +*** Using settings from env: settings.main +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: + Applying contenttypes.0001_initial... OK + Applying auth.0001_initial... OK + Applying account.0001_initial... OK + Applying account.0002_email_max_length... OK + Applying account.0003_alter_emailaddress_create_unique_verified_email... OK + Applying account.0004_alter_emailaddress_drop_unique_email... OK + Applying account.0005_emailaddress_idx_upper_email... OK + Applying account.0006_emailaddress_lower... OK + Applying account.0007_emailaddress_idx_email... OK + Applying account.0008_emailaddress_unique_primary_email_fixup... OK + Applying account.0009_emailaddress_unique_primary_email... OK + Applying actstream.0001_initial... OK + Applying actstream.0002_remove_action_data... OK + Applying actstream.0003_add_follow_flag... OK + 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 '' +Installed 3 object(s) from 1 fixture(s) +Running in production mode, running collectstatic now +level=INFO ts=2026-10-08 08:31:51,700 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 08:32:06,548 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 08:32:09,710 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.bench.invalid +Using gunicorn on port 8000... +level=INFO ts=2026-10-08 08:32:12,670 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 08:32:13 +0200] [28] [INFO] Starting gunicorn 26.1.0 +[2026-10-08 08:32:13 +0200] [28] [INFO] Listening at: http://0.0.0.0:8000 (28) +[2026-10-08 08:32:13 +0200] [28] [INFO] Using worker: sync +[2026-10-08 08:32:13 +0200] [29] [INFO] Booting worker with pid: 29 +[2026-10-08 08:32:13 +0200] [30] [INFO] Booting worker with pid: 30 +[2026-10-08 08:32:13 +0200] [28] [INFO] Control socket listening at /home/wger/.gunicorn/gunicorn.ctl +level=INFO ts=2026-10-08 08:32:14,549 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 08:32:14,551 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 06:27:14.291696+00:00 diff --git a/documentation/audits/day-2026-10-08/r762/bench/definition-docker-compose.yml b/documentation/audits/day-2026-10-08/r762/bench/definition-docker-compose.yml new file mode 100644 index 00000000..b19a8d5d --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/definition-docker-compose.yml @@ -0,0 +1,159 @@ +# wger - Edzésnapló és fitnesz tervező +# Domain: ${SUBDOMAIN}.${DOMAIN} +# Database: None (file-based) +# RAM: ~100M (mem_limit: 384M) | Pi-compatible: Yes +# +# Environment variables: +# DOMAIN - Your domain (e.g., demo-felhom.eu) +# SECRET_KEY - Titkosítási kulcs (auto-generated) + +services: + wger: + image: wger/server:2.7 + container_name: wger + # R-737 (2026-09-30): wger's app login API (the mobile app's) signs JWTs with JWT_PRIVATE_KEY / JWT_PUBLIC_KEY — an + # RSA pair the deploy's generators cannot make, and without it a CORRECT password answered 500. So the pair is made + # ONCE by wger's own `manage.py generate-jwt-keys`, kept 0600 on wger's own data volume (a restore brings the same + # key back), and loaded before the image's own entrypoint. Never printed. + entrypoint: + - /bin/sh + - -c + - | + K=/home/wger/db/.felhom-jwt.env + if [ ! -s "$$K" ]; then + (cd /home/wger/src && python3 manage.py generate-jwt-keys 2>/dev/null) | grep -E '^JWT_(PRIVATE|PUBLIC)_KEY=' > "$$K.tmp" + if [ "$$(grep -c . "$$K.tmp")" = 2 ]; then mv "$$K.tmp" "$$K" && chmod 600 "$$K"; else rm -f "$$K.tmp"; echo "felhom: JWT keys could not be made" >&2; fi + fi + if [ -s "$$K" ]; then set -a; . "$$K"; set +a; fi + exec /home/wger/entrypoint.sh + restart: unless-stopped + environment: + - TZ=Europe/Budapest + - SECRET_KEY=${SECRET_KEY} + # A wger 2.4+ a TELJES DJANGO_DB_* halmazt beolvassa, akkor is, ha az + # engine sqlite -- enélkül indulás nélkül kilép ("Set the DJANGO_DB_USER + # environment variable"). Az USER/PASSWORD/HOST/PORT értékeket az sqlite + # backend figyelmen kívül hagyja, de jelen kell lenniük. + # A DATABASE a wger_data kötetre mutat (/home/wger/db), oda, ahol a wger + # saját alapértelmezett sqlite fájlja is volt -- így meglévő telepítés + # adatai nem "tűnnek el" egy másik útvonalra. + # R-712 (measured 2026-09-29 on 9202): behind traefik wger saw the request as http and refused a browser's + # https Origin with "CSRF verification failed" — nobody could sign in from a browser. + - CSRF_TRUSTED_ORIGINS=https://${SUBDOMAIN}.${DOMAIN} + - X_FORWARDED_PROTO_HEADER_SET=True + # R-738: the image runs `manage.py migrate` at start ONLY with this switch (entrypoint.sh). Without it an update + # that brings migrations leaves wger serving its front page over an unmigrated database (login 500). + - DJANGO_PERFORM_MIGRATIONS=True + # R-752 (decided by CC unattended 2026-10-01, `09` §3 decision 58 — operator may reverse): django-axes locked by + # ip_address, and behind the tunnel every visitor has the tunnel's address (R-753) — a stranger's 10 wrong tries + # locked out EVERY household member for 30 min. Now only the targeted name, for 5 min (each try during a lock + # restarts it — wger 2.7 hard-codes that — so short is kinder), counted in the database (the default cache + # handler warns axes.W001). Measured on 9202: the other member unaffected; the targeted one in again at 7.5 min. + - AXES_LOCKOUT_PARAMETERS=username + - AXES_COOLOFF_TIME=5 + - AXES_HANDLER=axes.handlers.database.AxesDatabaseHandler + # R-763 (2026-10-05): the image defaults both to True. After the install the admin exists (after_install sets its + # password), so nobody needs wger's own sign-up: a stranger could make an account through the front door, and every + # anonymous visit to the dashboard made a guest user row (wger's middleware create_temporary_user). Both read by + # settings/main.py as env.bool (wger 2.7 L179-180); the sign-up view then redirects to the features page. + - ALLOW_REGISTRATION=False + - ALLOW_GUEST_USERS=False + # R-764 (2026-10-05): mail through the box's relay (smtp_mapping in .felhom.yml, tls_mode plaintext -> :2526). + # wger 2.7 settings/main.py:162 reads the EMAIL_* group ONLY when ENABLE_EMAIL is true, and then env.str() with + # no default on EMAIL_HOST_USER / EMAIL_HOST_PASSWORD — so both stay defined-EMPTY here (the relay takes no + # login; an absent one would stop wger at start). ENABLE_EMAIL is the gate: False unless the mail toggle injects + # "True". EMAIL_USE_TLS False: Django's STARTTLS verifies the certificate and the relay's is self-signed. + - ENABLE_EMAIL=${ENABLE_EMAIL:-False} + - EMAIL_HOST=${EMAIL_HOST:-} + - EMAIL_PORT=${EMAIL_PORT:-2526} + - EMAIL_HOST_USER= + - EMAIL_HOST_PASSWORD= + - EMAIL_USE_TLS=False + - EMAIL_USE_SSL=False + - FROM_EMAIL=${FROM_EMAIL:-wger Workout Manager } + - DJANGO_DB_ENGINE=django.db.backends.sqlite3 + - DJANGO_DB_DATABASE=/home/wger/db/database.sqlite + - DJANGO_DB_USER=wger + - DJANGO_DB_PASSWORD=wger + - DJANGO_DB_HOST=localhost + - DJANGO_DB_PORT=5432 + - SITE_URL=https://${SUBDOMAIN}.${DOMAIN} + # R-762 (2026-10-06): production mode, as upstream's own prod.env (wger-project/docker config/prod.env). With + # DJANGO_DEBUG=False the image's entrypoint runs `collectstatic` at every start (entrypoint.sh:27) into + # /home/wger/static, and Django stops serving /static and /media itself (it never did in production — upstream + # puts nginx in front). wger-files below serves both from the shared volumes. + - DJANGO_DEBUG=False + # R-762 (2026-10-08): the real web server. The image's entrypoint.sh runs `gunicorn wger.wsgi:application --preload + # --bind 0.0.0.0:$PORT` when WGER_USE_GUNICORN is "True" (else Django's development server, `manage.py runserver`). + # It passes no -w and the image has no gunicorn.conf.py, so the worker count is gunicorn's own WEB_CONCURRENCY + # (unset = 1). Two workers: measured on the bench and on 9202 (2 x "Booting worker" in the log), anon peak well + # under the 384M limit — with --preload the workers share the app's pages with the master. + - WGER_USE_GUNICORN=True + - WEB_CONCURRENCY=2 + volumes: + - wger_data:/home/wger/db + - wger_media:/home/wger/media + - wger_static:/home/wger/static + networks: + - traefik-public + deploy: + resources: + limits: + memory: 384M + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8000"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + labels: + - "traefik.enable=true" + - "traefik.http.routers.wger.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)" + - "traefik.http.routers.wger.entrypoints=websecure" + - "traefik.http.routers.wger.tls=true" + - "traefik.http.routers.wger.tls.certresolver=letsencrypt" + - "traefik.http.services.wger.loadbalancer.server.port=8000" + + # R-762 (2026-10-06): the file server upstream's production compose puts in front of wger (its `nginx` service and + # config/nginx.conf: `location /static/ { alias /wger/static/; }`, `location /media/ { alias /wger/media/; }`). + # Here traefik is already the front door, so traefik sends ONLY /static/ and /media/ to this nginx and everything + # else to wger as before — no config file is needed: nginx's stock config serves /usr/share/nginx/html, and the two + # volumes are mounted there read-only. Every gate the box puts in front of the app wraps EVERY router of the stack + # (stacks/setup_gate.go, family_gate.go), so this router is gated exactly like wger's own. + wger-files: + image: nginx:1.30.5-alpine + container_name: wger-files + restart: unless-stopped + depends_on: + - wger + volumes: + - wger_static:/usr/share/nginx/html/static:ro + - wger_media:/usr/share/nginx/html/media:ro + networks: + - traefik-public + deploy: + resources: + limits: + memory: 32M + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + labels: + - "traefik.enable=true" + - "traefik.http.routers.wger-files.rule=Host(`${SUBDOMAIN}.${DOMAIN}`) && (PathPrefix(`/static/`) || PathPrefix(`/media/`))" + - "traefik.http.routers.wger-files.entrypoints=websecure" + - "traefik.http.routers.wger-files.tls=true" + - "traefik.http.routers.wger-files.tls.certresolver=letsencrypt" + - "traefik.http.services.wger-files.loadbalancer.server.port=80" + +volumes: + wger_data: + wger_media: + wger_static: + +networks: + traefik-public: + external: true diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/abort-states.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/abort-states.json new file mode 100644 index 00000000..5d1f0247 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/abort-states.json @@ -0,0 +1,14 @@ +{ + "wger": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + }, + "wger-files": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/compose-final.log b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/compose-final.log new file mode 100644 index 00000000..923e802a --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/compose-final.log @@ -0,0 +1,81 @@ +wger-files | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration +wger-files | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh +wger-files | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf +wger-files | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf +wger-files | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh +wger-files | /docker-entrypoint.sh: Configuration complete; ready for start up +wger-files | 2026/10/08 06:18:54 [notice] 1#1: using the "epoll" event method +wger-files | 2026/10/08 06:18:54 [notice] 1#1: nginx/1.30.5 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: built by gcc 15.2.0 (Alpine 15.2.0) +wger-files | 2026/10/08 06:18:54 [notice] 1#1: OS: Linux 7.0.14-20-pve +wger-files | 2026/10/08 06:18:54 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 524288:524288 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker processes +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 30 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 31 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 32 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 33 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 34 +wger-files | 2026/10/08 06:18:54 [notice] 1#1: start worker process 35 +wger-files | 127.0.0.1 - - [08/Oct/2026:06:19:24 +0000] "GET / HTTP/1.1" 200 896 "-" "Wget" "-" +wger-files | 127.0.0.1 - - [08/Oct/2026:06:19:54 +0000] "GET / HTTP/1.1" 200 896 "-" "Wget" "-" +wger | *** Using settings from env: settings.main +wger | level=INFO ts=2026-10-08 08:18:55,562 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 +wger | Running in production mode, running collectstatic now +wger | level=INFO ts=2026-10-08 08:18:57,102 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 +wger | +wger | 22725 static files deleted, 11362 static files copied to '/home/wger/static', 11362 post-processed. +wger | Performing database migrations +wger | level=INFO ts=2026-10-08 08:19:23,909 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 +wger | System check identified some issues: +wger | +wger | WARNINGS: +wger | ?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies. +wger | HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS. +wger | Operations to perform: +wger | 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 +wger | Running migrations: +wger | No migrations to apply. +wger | Your models in app(s): 'exercises', 'gallery' have changes that are not yet reflected in a migration, and so won't be applied. +wger | Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them. +wger | level=INFO ts=2026-10-08 08:19:27,332 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 +wger | System check identified some issues: +wger | +wger | WARNINGS: +wger | ?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies. +wger | HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS. +wger | Set site URL to fitness.gate.invalid +wger | Using django's development server on port 8000... +wger | level=INFO ts=2026-10-08 08:19:29,776 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 +wger | level=INFO ts=2026-10-08 08:19:31,051 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 +wger | level=INFO ts=2026-10-08 08:19:31,063 module=autoreload path=/home/wger/.local/lib/python3.12/site-packages/django/utils/autoreload.py line=681 message=Watching for file changes with StatReloader +wger | Performing system checks... +wger | +wger | System check identified some issues: +wger | +wger | WARNINGS: +wger | ?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies. +wger | HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS. +wger | +wger | System check identified 1 issue (0 silenced). +wger | October 08, 2026 - 08:19:32 +wger | Django version 6.0.8, using settings 'settings.main' +wger | Starting development server at http://0.0.0.0:8000/ +wger | Quit the server with CONTROL-C. +wger | +wger | WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead. +wger | For more information on production servers see: https://docs.djangoproject.com/en/6.0/howto/deployment/ +wger | [08/Oct/2026 08:19:54] "HEAD / HTTP/1.1" 302 0 +wger | [08/Oct/2026 08:19:54] "HEAD /en/ HTTP/1.1" 302 0 +wger | [08/Oct/2026 08:19:54] "HEAD /en/software/features HTTP/1.1" 200 0 +wger | [08/Oct/2026 08:19:56] "GET /en/user/login HTTP/1.1" 200 43827 +wger | level=WARNING ts=2026-10-08 08:19:56,238 module=log path=/home/wger/.local/lib/python3.12/site-packages/django/utils/log.py line=249 message=Forbidden: /api/v2/weightentry/ +wger | [08/Oct/2026 08:19:56] "GET /api/v2/weightentry/?weight=105.51 HTTP/1.1" 403 58 +wger | [08/Oct/2026 08:19:56] "GET /en/user/login HTTP/1.1" 200 43827 +wger | level=INFO ts=2026-10-08 08:19:56,495 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"}. +wger | level=INFO ts=2026-10-08 08:19:56,504 module=database path=/home/wger/.local/lib/python3.12/site-packages/axes/handlers/database.py line=425 message=AXES: Cleaned up 1 expired access attempts from database that were older than 2026-10-08 06:14:56.348758+00:00 +wger | [08/Oct/2026 08:19:56] "POST /en/user/login HTTP/1.1" 302 0 +wger | [08/Oct/2026 08:19:56] "GET /api/v2/weightentry/?weight=199.99 HTTP/1.1" 200 52 +wger | [08/Oct/2026 08:19:56] "GET /api/v2/weightentry/?weight=105.51 HTTP/1.1" 200 159 diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/engine-state.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/engine-state.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/engine-state.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after-detail.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after-detail.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after-detail.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-after.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before-detail.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before-detail.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before-detail.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/files-before.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/memory-samples.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/memory-samples.json new file mode 100644 index 00000000..839f61a7 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/memory-samples.json @@ -0,0 +1,1282 @@ +[ + { + "t": 15.2, + "containers": { + "wger": { + "limit": 402653184, + "current": 302706688, + "peak": 402653184, + "anon": 173158400, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6500352, + "peak": 8560640, + "anon": 5152768, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 292 + }, + { + "t": 30.3, + "containers": { + "wger": { + "limit": 402653184, + "current": 306049024, + "peak": 402653184, + "anon": 176492544, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6537216, + "peak": 8859648, + "anon": 5189632, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 580 + }, + { + "t": 45.5, + "containers": { + "wger": { + "limit": 402653184, + "current": 306647040, + "peak": 402653184, + "anon": 176586752, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6537216, + "peak": 8859648, + "anon": 5189632, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 872 + }, + { + "t": 60.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 305811456, + "peak": 402653184, + "anon": 176775168, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6578176, + "peak": 8859648, + "anon": 5226496, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1160 + }, + { + "t": 75.8, + "containers": { + "wger": { + "limit": 402653184, + "current": 306663424, + "peak": 402653184, + "anon": 176865280, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6578176, + "peak": 8859648, + "anon": 5226496, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1452 + }, + { + "t": 91.0, + "containers": { + "wger": { + "limit": 402653184, + "current": 307011584, + "peak": 402653184, + "anon": 176955392, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6615040, + "peak": 8859648, + "anon": 5263360, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1740 + }, + { + "t": 106.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 306061312, + "peak": 402653184, + "anon": 177020928, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6615040, + "peak": 8859648, + "anon": 5263360, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2028 + }, + { + "t": 121.3, + "containers": { + "wger": { + "limit": 402653184, + "current": 307286016, + "peak": 402653184, + "anon": 177205248, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2320 + }, + { + "t": 136.4, + "containers": { + "wger": { + "limit": 402653184, + "current": 307032064, + "peak": 402653184, + "anon": 177229824, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2608 + }, + { + "t": 151.6, + "containers": { + "wger": { + "limit": 402653184, + "current": 306372608, + "peak": 402653184, + "anon": 177299456, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2896 + }, + { + "t": 166.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 307388416, + "peak": 402653184, + "anon": 177324032, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3188 + }, + { + "t": 181.9, + "containers": { + "wger": { + "limit": 402653184, + "current": 307167232, + "peak": 402653184, + "anon": 177364992, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3476 + }, + { + "t": 197.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 307724288, + "peak": 402653184, + "anon": 177389568, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 8859648, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3768 + }, + { + "t": 212.2, + "containers": { + "wger": { + "limit": 402653184, + "current": 307523584, + "peak": 402653184, + "anon": 177455104, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4056 + }, + { + "t": 227.4, + "containers": { + "wger": { + "limit": 402653184, + "current": 306765824, + "peak": 402653184, + "anon": 177471488, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4344 + }, + { + "t": 242.5, + "containers": { + "wger": { + "limit": 402653184, + "current": 307339264, + "peak": 402653184, + "anon": 177528832, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4636 + }, + { + "t": 257.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 307105792, + "peak": 402653184, + "anon": 177549312, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4924 + }, + { + "t": 272.9, + "containers": { + "wger": { + "limit": 402653184, + "current": 307392512, + "peak": 402653184, + "anon": 177573888, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5216 + }, + { + "t": 288.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 307134464, + "peak": 402653184, + "anon": 177586176, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5504 + }, + { + "t": 303.2, + "containers": { + "wger": { + "limit": 402653184, + "current": 306655232, + "peak": 402653184, + "anon": 177618944, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5792 + }, + { + "t": 318.4, + "containers": { + "wger": { + "limit": 402653184, + "current": 307695616, + "peak": 402653184, + "anon": 177627136, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6084 + }, + { + "t": 333.5, + "containers": { + "wger": { + "limit": 402653184, + "current": 307204096, + "peak": 402653184, + "anon": 177659904, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6372 + }, + { + "t": 348.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 307757056, + "peak": 402653184, + "anon": 177676288, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6664 + }, + { + "t": 363.8, + "containers": { + "wger": { + "limit": 402653184, + "current": 307519488, + "peak": 402653184, + "anon": 177700864, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6952 + }, + { + "t": 379.0, + "containers": { + "wger": { + "limit": 402653184, + "current": 307511296, + "peak": 402653184, + "anon": 177725440, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7240 + }, + { + "t": 394.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 307589120, + "peak": 402653184, + "anon": 177774592, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7529 + }, + { + "t": 409.3, + "containers": { + "wger": { + "limit": 402653184, + "current": 307560448, + "peak": 402653184, + "anon": 177790976, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7820 + }, + { + "t": 424.4, + "containers": { + "wger": { + "limit": 402653184, + "current": 307621888, + "peak": 402653184, + "anon": 177823744, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8108 + }, + { + "t": 439.6, + "containers": { + "wger": { + "limit": 402653184, + "current": 308170752, + "peak": 402653184, + "anon": 177852416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8400 + }, + { + "t": 454.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 306941952, + "peak": 402653184, + "anon": 177909760, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8688 + }, + { + "t": 469.9, + "containers": { + "wger": { + "limit": 402653184, + "current": 307994624, + "peak": 402653184, + "anon": 177926144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9019392, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8978 + }, + { + "t": 485.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 307265536, + "peak": 402653184, + "anon": 177946624, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9268 + }, + { + "t": 500.2, + "containers": { + "wger": { + "limit": 402653184, + "current": 307417088, + "peak": 402653184, + "anon": 177946624, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9556 + }, + { + "t": 515.4, + "containers": { + "wger": { + "limit": 402653184, + "current": 307802112, + "peak": 402653184, + "anon": 177991680, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9848 + }, + { + "t": 530.5, + "containers": { + "wger": { + "limit": 402653184, + "current": 307564544, + "peak": 402653184, + "anon": 177995776, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10136 + }, + { + "t": 545.7, + "containers": { + "wger": { + "limit": 402653184, + "current": 308133888, + "peak": 402653184, + "anon": 178057216, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10428 + }, + { + "t": 560.8, + "containers": { + "wger": { + "limit": 402653184, + "current": 307884032, + "peak": 402653184, + "anon": 178085888, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10716 + }, + { + "t": 576.0, + "containers": { + "wger": { + "limit": 402653184, + "current": 308174848, + "peak": 402653184, + "anon": 178094080, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11008 + }, + { + "t": 591.1, + "containers": { + "wger": { + "limit": 402653184, + "current": 307650560, + "peak": 402653184, + "anon": 178106368, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11296 + }, + { + "t": 606.3, + "containers": { + "wger": { + "limit": 402653184, + "current": 307240960, + "peak": 402653184, + "anon": 178151424, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + }, + "wger-files": { + "limit": 33554432, + "current": 6660096, + "peak": 9281536, + "anon": 5308416, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11584 + } +] \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/migration-lines.txt b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/migration-lines.txt new file mode 100644 index 00000000..b0f33169 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/migration-lines.txt @@ -0,0 +1,6 @@ +wger | Performing database migrations +wger | 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 +wger | Running migrations: +wger | No migrations to apply. +wger | Your models in app(s): 'exercises', 'gallery' have changes that are not yet reflected in a migration, and so won't be applied. +wger | Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them. \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/run.log b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/run.log new file mode 100644 index 00000000..efd6c22b --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/run.log @@ -0,0 +1,60 @@ +[06:05:43] scratch drive folders cleared before FROM (R-656): none existed +[06:05:43] MV-wger: deploying wger at FROM {'wger': 'wger/server:2.7', 'wger-files': 'nginx:1.30.5-alpine'} +[06:07:17] FROM settled=True in 93.0s :: {"wger": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}, "wger-files": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[06:07:17] fixture: the BOX walk's own (Wger), through upgrade_boxport +[06:07:17] wger: the generated admin password does not log in (POST /en/user/login -> 200) — running the template's own after_install command (the app's CLI, as the product does after an install) +[06:07:19] wger: after_install :: version 8.3.1, blocking by username FELHOM_AFTER_INSTALL_OK +[06:07:21] wger: POST /api/v2/weightentry/ http=201 +[06:07:21] wger: readback of the seeded weight entry http=200 found=True +[06:07:21] C1 (seed reads back BEFORE): True +[06:07:21] MV-wger: swapping to TO {'wger': 'wger/server:2.7', 'wger-files': 'nginx:1.30.5-alpine'} +[06:07:33] TO up -d rc=0 +[06:08:35] TO settled=True in 62.1s :: {"wger": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}, "wger-files": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[06:08:35] migration lines observed: 6 +[06:08:35] wger: readback of the seeded weight entry http=200 found=True +[06:08:35] RESULT (seed reads back AFTER): True +[06:08:36] memory watch: 600s, 4 callers on 1 path(s) at 172.18.0.2:8000 +[06:08:51] + 15s wger=288M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=292 +[06:09:06] + 30s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=580 +[06:09:21] + 46s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=872 +[06:09:37] + 61s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1160 +[06:09:52] + 76s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1452 +[06:10:07] + 91s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=1740 +[06:10:22] + 106s wger=291M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2028 +[06:10:37] + 121s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2320 +[06:10:52] + 136s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2608 +[06:11:07] + 152s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=2896 +[06:11:23] + 167s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3188 +[06:11:38] + 182s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3476 +[06:11:53] + 197s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=3768 +[06:12:08] + 212s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4056 +[06:12:23] + 227s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4344 +[06:12:38] + 242s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4636 +[06:12:54] + 258s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=4924 +[06:13:09] + 273s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5216 +[06:13:24] + 288s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5504 +[06:13:39] + 303s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=5792 +[06:13:54] + 318s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6084 +[06:14:09] + 334s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6372 +[06:14:25] + 349s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6664 +[06:14:40] + 364s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=6952 +[06:14:55] + 379s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7240 +[06:15:10] + 394s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7529 +[06:15:25] + 409s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=7820 +[06:15:40] + 424s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8108 +[06:15:55] + 440s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8400 +[06:16:11] + 455s wger=292M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8688 +[06:16:26] + 470s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=8978 +[06:16:41] + 485s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9268 +[06:16:56] + 500s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9556 +[06:17:11] + 515s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=9848 +[06:17:26] + 530s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10136 +[06:17:42] + 546s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10428 +[06:17:57] + 561s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=10716 +[06:18:12] + 576s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11008 +[06:18:27] + 591s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11296 +[06:18:42] + 606s wger=293M/384M peak=384M kills=0 rs=0 wger-files=6M/32M peak=8M kills=0 rs=0 reqs=11584 +[06:18:42] memory watch: killed=False tight=[] requests=11584 codes={'302': 11584} +[06:18:42] MV-wger: ABORT — putting the FROM images back +[06:19:56] wger: readback of the seeded weight entry http=200 found=True +[06:19:56] ABORT: app came back in 62.0s; data present=True \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-full.log b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-full.log new file mode 100644 index 00000000..4e1cbabc --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-full.log @@ -0,0 +1,58 @@ +wger-files | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration +wger-files | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh +wger-files | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf +wger-files | 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf +wger-files | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh +wger-files | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh +wger-files | /docker-entrypoint.sh: Configuration complete; ready for start up +wger-files | 2026/10/08 06:07:33 [notice] 1#1: using the "epoll" event method +wger-files | 2026/10/08 06:07:33 [notice] 1#1: nginx/1.30.5 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: built by gcc 15.2.0 (Alpine 15.2.0) +wger | *** Using settings from env: settings.main +wger-files | 2026/10/08 06:07:33 [notice] 1#1: OS: Linux 7.0.14-20-pve +wger | level=INFO ts=2026-10-08 08:07:35,364 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 +wger | Running in production mode, running collectstatic now +wger | level=INFO ts=2026-10-08 08:07:37,454 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 +wger | +wger | 22725 static files deleted, 11362 static files copied to '/home/wger/static', 11362 post-processed. +wger | Performing database migrations +wger | level=INFO ts=2026-10-08 08:08:06,559 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 +wger | System check identified some issues: +wger | +wger | WARNINGS: +wger | ?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies. +wger | HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS. +wger | Operations to perform: +wger-files | 2026/10/08 06:07:33 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 524288:524288 +wger | 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 +wger | Running migrations: +wger | No migrations to apply. +wger | Your models in app(s): 'exercises', 'gallery' have changes that are not yet reflected in a migration, and so won't be applied. +wger | Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them. +wger | level=INFO ts=2026-10-08 08:08:10,187 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 +wger | System check identified some issues: +wger | +wger | WARNINGS: +wger | ?: (axes.W006) AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'. This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies. +wger | HINT: Add 'ip_address' to AXES_LOCKOUT_PARAMETERS. +wger | Set site URL to fitness.gate.invalid +wger | Using gunicorn on port 8000... +wger | level=INFO ts=2026-10-08 08:08:12,859 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 +wger | [2026-10-08 08:08:13 +0200] [17] [INFO] Starting gunicorn 26.1.0 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker processes +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 30 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 31 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 32 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 33 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 34 +wger-files | 2026/10/08 06:07:33 [notice] 1#1: start worker process 35 +wger-files | 127.0.0.1 - - [08/Oct/2026:06:08:03 +0000] "GET / HTTP/1.1" 200 896 "-" "Wget" "-" +wger-files | 127.0.0.1 - - [08/Oct/2026:06:08:33 +0000] "GET / HTTP/1.1" 200 896 "-" "Wget" "-" +wger | [2026-10-08 08:08:13 +0200] [17] [INFO] Listening at: http://0.0.0.0:8000 (17) +wger | [2026-10-08 08:08:13 +0200] [17] [INFO] Using worker: sync +wger | [2026-10-08 08:08:13 +0200] [18] [INFO] Booting worker with pid: 18 +wger | [2026-10-08 08:08:13 +0200] [19] [INFO] Booting worker with pid: 19 +wger | [2026-10-08 08:08:13 +0200] [17] [INFO] Control socket listening at /home/wger/.gunicorn/gunicorn.ctl +wger | level=WARNING ts=2026-10-08 08:08:33,548 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. diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-states.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-states.json new file mode 100644 index 00000000..5d1f0247 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/to-states.json @@ -0,0 +1,14 @@ +{ + "wger": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + }, + "wger-files": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/verdict.json b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/verdict.json new file mode 100644 index 00000000..3e99ea98 --- /dev/null +++ b/documentation/audits/day-2026-10-08/r762/bench/evidence/MV-wger/verdict.json @@ -0,0 +1,70 @@ +{ + "harness_version": 5, + "edge": "MV-wger", + "app": "wger", + "note": "definition step to wger@gunicorn", + "from": { + "wger": "wger/server:2.7", + "wger-files": "nginx:1.30.5-alpine" + }, + "to": { + "wger": "wger/server:2.7", + "wger-files": "nginx:1.30.5-alpine" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": "\u001b[2Kwger | Performing database migrations", + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 606.5, + "requested_s": 600, + "requests": 11584, + "codes": { + "302": 11584 + }, + "first_kill": null, + "containers": { + "wger": { + "limit": 402653184, + "peak": 402653184, + "peak_pct": 1.0, + "anon_peak_sampled": 178151424, + "anon_peak_pct": 0.442, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + }, + "wger-files": { + "limit": 33554432, + "peak": 9281536, + "peak_pct": 0.277, + "anon_peak_sampled": 5308416, + "anon_peak_pct": 0.158, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 62.1, + "measured_at": "2026-10-08T06:19:56Z", + "evidence": "evidence/MV-wger", + "scratch_cleared": [], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 853.6 +} \ No newline at end of file diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 5ae95f42..81577252 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -117,7 +117,7 @@ stopping line that lies. | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| | **R-250** | Install & onboarding | P3 | **A customer create can fail fail-closed because the host-key scan ladder is shorter than the DNS/AAAA settle time.** Found 2026-08-07 creating the fifth walk's venue. `POST /configs/new` with off-site enabled provisions the Storage Box sub-account and then scans its SSH host key to pin it — **fail-closed by design** (`offsite.go:111-121`, *"don't serve a descriptor the controller can't verify"*), with `defaultScanBackoff` = 2+4+8+16+30 ≈ **60 s**, sized by its own comment to *"the observed DNS propagation lag"*. **Measured, both halves:** the first create exhausted the ladder — five `no such host`, then `dial tcp [2a01:4f8:bacc:2:200::d30]:23: connect: network is unreachable` (the name had just begun resolving, **AAAA-first, into a pod with no IPv6 route**) — and the hub logged `[ERROR] offsite provision for walk5`. An **identical second POST succeeded** ~70 s later on its own final rung (`shared already provisioned … (subaccount 285351)`). **Total settle ≈ 100 s against a 60 s budget.** The endpoint was never the problem: verified afterwards from both the node and the hub pod, by name, `SSH-2.0-OpenSSH_9.6p1`. **Two distinct things are wrong and should not be merged:** (1) the budget is sized against DNS *existence*, but what actually bit is the **AAAA-before-A** window, a different and longer phenomenon — lengthen the ladder and/or prefer the A record for the scan dial; (2) **the operator is told nothing actionable** — the create fails with a generic error, the remedy is "press it again", and nothing says so. The retry is genuinely safe (idempotent on the label; confirmed afterwards — `one_time_secrets` 1, one sub-account, no double-provision) **but that safety is invisible to the person deciding whether pressing again will double-charge them.** **Severity LOW-MEDIUM:** self-clearing, no data at risk — but it is the first thing a new customer's provisioning does and it fails looking like an outage. | **READY** — owner Viktor | — | — | operator | -| **R-516** | Install & onboarding | P3 | **[P3-LOW] English a customer meets on a fresh box and its apps' first screens — enumerated by the big night.** MEASURED 2026-09-14 (BIGNIGHT, VM 333, ISO 1.27.1, controller 0.242.0). Felhom-owned: (1) the dashboard menu item **„Debug"**; (2) the dashboard CPU tile **„Load: 0.29 / 0.39 / 0.37"**; (3) the launcher tile **„Filebrowser"** opens a login in English with no Felhom text (R-513); (4) the storage page mixes formal „Adjon hozzá / Csatlakoztasson" with the product's „te". App first screens a household meets before any Felhom text helps: (5) **Uptime Kuma 2.4 opens on „Which database would you like to use?"** (SQLite / Embedded MariaDB, „Next") — the app card's „Első lépések" does not mention it; (6) PrivateBin, Gokapi, AdventureLog and FileBrowser UIs are English (the apps' own). Already rows: the Proxmox installer screens (R-495, answered by the guide), `wiki.DOMAIN` (R-498). **Fix shape:** rename „Debug"/„Load" (controller); add the Uptime Kuma database step to its card, or pre-seed `db-config.json` for SQLite in the template (catalog). **Added by F4 (20:04:54Z):** (7) the storage page prints the disconnect time as a raw ISO UTC string „Leválasztva: 2026-09-14T19:58:02Z"; (8) the „Meghajtó leválasztva" banner appears twice on every page; (9) „4 telepített alkalmazás nem fut — nézze meg a rendszermonitort" uses the formal form. **Added by F7 (20:50–21:00Z, system disk at 95 %):** (10) a banner on every page in English, „**SSD disk usage high: 90%**"; (11) the dashboard tile reads „Rendszer (/) 61.8 GB / 68.7 GB (**90%**)" while `df` reports **95 %** (reserved blocks ignored), and „(/)" labels the data volume `/mnt/sys_drive`; the deploy page says nothing about free disk. **Added by the i18n spike (2026-09-17, controller v0.247.0):** (12) **six formal („ön") forms in the converted dashboard copy** — „Olvassa be telefonnal" (launcher QR hint), „Biztosan kikapcsolja a megosztást?" (launcher), „Ha újratelepíti … importálnia kell" (the layout's remove-app modal), „Kérjük, vegye fel a kapcsolatot" (backups empty state). They were NOT fixed — a localisation release may not change Hungarian bytes — and are now COUNTED by `controller/scripts/i18n_missing_gate.py` (`HU_FORMAL_CEILING` = 6, a ratchet: a new one convicts, fixing one lowers it). The inventory (`audits/I18N-INVENTORY-2026-09-17.md`) is the list this row closes against in localisation slice 6 (R-561). **Extended 2026-09-17 by slice 1 (controller v0.248.0–v0.250.0):** the converted copy now counts **16** formal forms (`HU_FORMAL_CEILING` 6 → 10 → 12 → 16, each raise stated, Hungarian unchanged by rule) — and the count is an UNDER-count: the gate's stem list sees 4 on the release C pages (storage, drive wizards, debug), while a wider list („írja be”, „adja meg”, „adjon hozzá”, „válassza ki”, „biztosan eltávolítja”, „engedélyezze”, „hozzon létre”, „kattintson”) finds at least **22** keys there alone (login „Adja meg a jelszavát”, the NAS guide, the storage confirms). Widen the stems when this row is worked; the ceiling rises with them. **NARROWED 2026-09-20 by localisation slice 6's walk, item by item** (`audits/i18n-slice6-2026-09-20/R-516-item-by-item.md`): item 2 FIXED (slice 1), items 3 and 6 APP-OWNED (and for an English household item 6 is an advantage), item 11 is a wrong NUMBER rather than a language and needs its own row, item 1 is correct English for an English reader and still open for a Hungarian one. **Items 4, 7, 8, 9 and 10 are NOT DECIDABLE FROM AN ENGLISH WALK** - they are about Hungarian copy quality, a disconnected drive and a full disk, none of which this walk had. **What this row is now waiting for is a HUNGARIAN walk on a box with a second drive**, not another English one; saying it closed would be the kind of closure that makes a register stop meaning anything. | **NARROWED 2026-09-20 - rank P3-LOW; owner: CC; needs a Hungarian walk** **2026-10-05 (burn-down night): items 1 and 4 FIXED on controller `main`** (`d7efa7b`): the menu says „Hibakeresés” (English „Debug”); the storage pages use the te-form (formal-form ceiling 17 → 14; 116 parity fixtures changed by exactly those bytes). Item 2 was already fixed. **Left:** items 5–6 (apps' own screens) and the rest. **2026-10-06: items 1 and 4 DELIVERED** (controller v0.298.0). **2026-10-06 (burn-down night, later): items 7–10 and 12 FIXED on controller `main`** (`e4774e6`, `2e5ef7e`): the disconnect time in local time; one banner per unplugged drive; „nézd meg”; the disk/memory/CPU/temperature banners in the household's language (`health.*` keys; the wire text to the hub unchanged and pinned); the last eleven counted formal forms are te-form (ceiling 14 → 0; 119 parity fixtures changed by exactly those bytes). **Left:** items 5–6 (apps' own screens); about 20 formal forms the gate does not count yet („írja be”, „adja meg”, „hozzon létre”, …) on the debug, storage, network-storage and security pages. Item 11 moved to its own row **R-889**. **2026-10-06 02:40: the bundle has NO formal form left** (controller `4c3c203`, unreleased): 49 sentences in the te-form; the gate's stem list widened (it counted 61 on the previous bundle, 0 now) with decoys and one listed third-person exception. **Left (narrowed):** Felhom-owned formal forms in Go and template literals the gate cannot read yet — the network-storage attach errors, escrow and share handlers, the single-copy notice, a settings refusal, the SMART mail, a fill-watch „Kérjük”, and the setup wizard pages; each moves into the bundle in the te-form. Items 5–6 are the apps' own screens (not ours). **2026-10-06: items 7–10, 12 and the bundle's 49 formal forms DELIVERED** in controller v0.299.0. **2026-10-06 night: the Go-literal leftover moved** (controller main `3d6ba28`, unreleased): 22 Go-literal messages — escrow handler (6), share password page (2), export upload (1), network-storage uid/remove/attach failures (13) — are bundle keys (`api.escrow.*`, `api.share.*`, `api.export.*`, `api.netstorage.*`) in the te-form with English (14 were formal); a detached attach failure renders in the reader's language; the NAS refusal says „Válassz". `TestR516_GoLiteralFormalFormsAreGone` (source scan + positive control), `TestR516_NetAddFailureFollowsTheReader`; red-proofs `audits/night-burndown-2026-10-06/ctrl/R-516-red*.txt`. **Left:** the setup wizard and recovery-info.txt (R-554); the SMART and fill-watch texts (the hub event's message — wire text, needs the household-copy producer shape); items 5–6 (apps' own screens). | — | — | CC | +| **R-516** | Install & onboarding | P3 | **[P3-LOW] English a customer meets on a fresh box and its apps' first screens — enumerated by the big night.** MEASURED 2026-09-14 (BIGNIGHT, VM 333, ISO 1.27.1, controller 0.242.0). Felhom-owned: (1) the dashboard menu item **„Debug"**; (2) the dashboard CPU tile **„Load: 0.29 / 0.39 / 0.37"**; (3) the launcher tile **„Filebrowser"** opens a login in English with no Felhom text (R-513); (4) the storage page mixes formal „Adjon hozzá / Csatlakoztasson" with the product's „te". App first screens a household meets before any Felhom text helps: (5) **Uptime Kuma 2.4 opens on „Which database would you like to use?"** (SQLite / Embedded MariaDB, „Next") — the app card's „Első lépések" does not mention it; (6) PrivateBin, Gokapi, AdventureLog and FileBrowser UIs are English (the apps' own). Already rows: the Proxmox installer screens (R-495, answered by the guide), `wiki.DOMAIN` (R-498). **Fix shape:** rename „Debug"/„Load" (controller); add the Uptime Kuma database step to its card, or pre-seed `db-config.json` for SQLite in the template (catalog). **Added by F4 (20:04:54Z):** (7) the storage page prints the disconnect time as a raw ISO UTC string „Leválasztva: 2026-09-14T19:58:02Z"; (8) the „Meghajtó leválasztva" banner appears twice on every page; (9) „4 telepített alkalmazás nem fut — nézze meg a rendszermonitort" uses the formal form. **Added by F7 (20:50–21:00Z, system disk at 95 %):** (10) a banner on every page in English, „**SSD disk usage high: 90%**"; (11) the dashboard tile reads „Rendszer (/) 61.8 GB / 68.7 GB (**90%**)" while `df` reports **95 %** (reserved blocks ignored), and „(/)" labels the data volume `/mnt/sys_drive`; the deploy page says nothing about free disk. **Added by the i18n spike (2026-09-17, controller v0.247.0):** (12) **six formal („ön") forms in the converted dashboard copy** — „Olvassa be telefonnal" (launcher QR hint), „Biztosan kikapcsolja a megosztást?" (launcher), „Ha újratelepíti … importálnia kell" (the layout's remove-app modal), „Kérjük, vegye fel a kapcsolatot" (backups empty state). They were NOT fixed — a localisation release may not change Hungarian bytes — and are now COUNTED by `controller/scripts/i18n_missing_gate.py` (`HU_FORMAL_CEILING` = 6, a ratchet: a new one convicts, fixing one lowers it). The inventory (`audits/I18N-INVENTORY-2026-09-17.md`) is the list this row closes against in localisation slice 6 (R-561). **Extended 2026-09-17 by slice 1 (controller v0.248.0–v0.250.0):** the converted copy now counts **16** formal forms (`HU_FORMAL_CEILING` 6 → 10 → 12 → 16, each raise stated, Hungarian unchanged by rule) — and the count is an UNDER-count: the gate's stem list sees 4 on the release C pages (storage, drive wizards, debug), while a wider list („írja be”, „adja meg”, „adjon hozzá”, „válassza ki”, „biztosan eltávolítja”, „engedélyezze”, „hozzon létre”, „kattintson”) finds at least **22** keys there alone (login „Adja meg a jelszavát”, the NAS guide, the storage confirms). Widen the stems when this row is worked; the ceiling rises with them. **NARROWED 2026-09-20 by localisation slice 6's walk, item by item** (`audits/i18n-slice6-2026-09-20/R-516-item-by-item.md`): item 2 FIXED (slice 1), items 3 and 6 APP-OWNED (and for an English household item 6 is an advantage), item 11 is a wrong NUMBER rather than a language and needs its own row, item 1 is correct English for an English reader and still open for a Hungarian one. **Items 4, 7, 8, 9 and 10 are NOT DECIDABLE FROM AN ENGLISH WALK** - they are about Hungarian copy quality, a disconnected drive and a full disk, none of which this walk had. **What this row is now waiting for is a HUNGARIAN walk on a box with a second drive**, not another English one; saying it closed would be the kind of closure that makes a register stop meaning anything. **-- 2026-10-08:** the recovery screen's own answers are Go literals in Hungarian only — 14 `renderRecovery` messages in `controller/internal/web/recovery_handlers.go` (seen: the safe default „A művelet nem fejeződött be…" rendered on an English page in a red-proof run). Today's new R-304 sentence is a bundle key in both languages; the 14 old ones are not. | **NARROWED 2026-09-20 - rank P3-LOW; owner: CC; needs a Hungarian walk** **2026-10-05 (burn-down night): items 1 and 4 FIXED on controller `main`** (`d7efa7b`): the menu says „Hibakeresés” (English „Debug”); the storage pages use the te-form (formal-form ceiling 17 → 14; 116 parity fixtures changed by exactly those bytes). Item 2 was already fixed. **Left:** items 5–6 (apps' own screens) and the rest. **2026-10-06: items 1 and 4 DELIVERED** (controller v0.298.0). **2026-10-06 (burn-down night, later): items 7–10 and 12 FIXED on controller `main`** (`e4774e6`, `2e5ef7e`): the disconnect time in local time; one banner per unplugged drive; „nézd meg”; the disk/memory/CPU/temperature banners in the household's language (`health.*` keys; the wire text to the hub unchanged and pinned); the last eleven counted formal forms are te-form (ceiling 14 → 0; 119 parity fixtures changed by exactly those bytes). **Left:** items 5–6 (apps' own screens); about 20 formal forms the gate does not count yet („írja be”, „adja meg”, „hozzon létre”, …) on the debug, storage, network-storage and security pages. Item 11 moved to its own row **R-889**. **2026-10-06 02:40: the bundle has NO formal form left** (controller `4c3c203`, unreleased): 49 sentences in the te-form; the gate's stem list widened (it counted 61 on the previous bundle, 0 now) with decoys and one listed third-person exception. **Left (narrowed):** Felhom-owned formal forms in Go and template literals the gate cannot read yet — the network-storage attach errors, escrow and share handlers, the single-copy notice, a settings refusal, the SMART mail, a fill-watch „Kérjük”, and the setup wizard pages; each moves into the bundle in the te-form. Items 5–6 are the apps' own screens (not ours). **2026-10-06: items 7–10, 12 and the bundle's 49 formal forms DELIVERED** in controller v0.299.0. **2026-10-06 night: the Go-literal leftover moved** (controller main `3d6ba28`, unreleased): 22 Go-literal messages — escrow handler (6), share password page (2), export upload (1), network-storage uid/remove/attach failures (13) — are bundle keys (`api.escrow.*`, `api.share.*`, `api.export.*`, `api.netstorage.*`) in the te-form with English (14 were formal); a detached attach failure renders in the reader's language; the NAS refusal says „Válassz". `TestR516_GoLiteralFormalFormsAreGone` (source scan + positive control), `TestR516_NetAddFailureFollowsTheReader`; red-proofs `audits/night-burndown-2026-10-06/ctrl/R-516-red*.txt`. **Left:** the setup wizard and recovery-info.txt (R-554); the SMART and fill-watch texts (the hub event's message — wire text, needs the household-copy producer shape); items 5–6 (apps' own screens). | — | — | CC | | **R-554** | Install & onboarding | P3 | **[P3-LOW] Delete the first-boot setup wizard — obsolete by design, still reachable.** OPERATOR DECISION 2026-09-17 (localisation starter, decision 4: „out of scope, obsolete"). `02-controller-module-map.md` L56 calls `internal/setup/` obsolete; `cmd/controller/main.go` L322 still enters it when `setup.NeedsSetup(cfg)` — `customer.id` empty after bootstrap ingestion, or a `.needs-setup` marker (`internal/setup/setup.go` L17-25). Ingestion leaves `customer.id` empty on a missing/invalid `bootstrap.json`, a failed hub pull, or a failed merge/write/reload (`internal/bootstrap/bootstrap.go` L109-163) — so a box whose first boot cannot reach the hub shows a household an 8-page wizard (95 Hungarian strings, its own template set and CSRF). **Fix shape:** decide what such a box shows instead (a single „cannot reach Felhom yet, retrying" page — needs no decision beyond copy), then delete `internal/setup/` and `runSetupMode`; red-proof that a failed ingestion renders the waiting page, not a 404. **Check first** whether any drill/golden path still relies on `.needs-setup`. | **READY - rank P3-LOW; owner: CC** **2026-10-06 night: stopped — needs the operator:** deleting the wizard makes the household's `recovery-info.txt` (`internal/recovery/info.go:25-47`, „docker-setup.sh … :8081 … Visszaállítás mentésből") false; what that file should say instead is a customer promise. The debug route `debugTriggerSetupWizard` (`handler_debug.go:690`) is the only `.needs-setup` writer; no drill/golden uses it. | — | — | CC | | **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC | | **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator | @@ -128,7 +128,7 @@ stopping line that lies. |---|---|---|---|---|---|---|---| | **R-562** | Apps & catalog | P3 | **[P3-LOW] Dates and sizes are not formatted for any locale — and the Hungarian pages disagree with themselves.** FOUND 2026-09-17 by the i18n inventory §2.8: the two template date layouts differ (`2006. 01. 02. 15:04` Hungarian vs `2006-01-02 15:04` ISO); 10 layout literals in `internal/web` Go and 25 elsewhere pick formats ad hoc; sizes print a decimal POINT (`%.1f GB`, 4 helpers) where Hungarian uses a comma; `timeAgo`/`nextRunLabel`/`pruneLabel` produce Hungarian words outside the three converted pages. Not changed by v0.247.0 (Hungarian bytes are frozen by the parity rule). **Fix shape:** one date and one size formatter per language in `internal/i18n`, the Hungarian output deliberately changed in ONE reviewed release with the parity fixtures re-captured for that release only and the change named in its CHANGELOG. Needs an operator word on the Hungarian format (comma, date style). | **READY - rank P3-LOW; owner: CC** **2026-10-06 night: not started — the row needs the operator's word on the Hungarian format (decimal comma, date style) before any code.** | — | — | CC | | **R-676** | Apps & catalog | P3 | **[P3-LOW] Watch: immich's first start restarted 12 times — decision 28's crash-loop stop (6 in 10 min) would stop it.** From the 2026-09-17 chaos night (DB connection dropped during the first-start geocoding import on a 6 GB guest; it did not recover that night). No healthy app in any drill evidence restarts on a first start (1831 samples, 40 live containers), so the threshold stands; this row exists so the first immich install under v0.269.x is watched. `audits/night-2026-09-24/A3/40-first-start-restarts.txt` **2026-09-25 night (read from source, v0.271.0): a DEPLOY's first start is NOT covered by decision 28's suppression** — `Deploying` clears when `compose up -d` returns (`deploy.go` "Clear deploying flag"), and `ObserveUnhealthy` then samples the app; an automatic update's step, verify and undo ARE covered (`Updating`, pinned by `TestD28_NoCrashLoopStopDuringAnAutomaticStep`). So a first start that restarts ≥ 6 times in 10 min is stopped — which R-676 already accepts for a broken first start; a healthy slow first start would be stopped too. **-- 2026-09-30: the first-start restarts are explained.** immich's first-start geodata import OOM-kills its database at 512M on a guest with no swap (R-732, measured: 61–104 kills); the 2026-09-17 chaos-night case (DB connection dropped during the import on a 6 GB guest) fits it. Fixed in the catalog (`56c4888`, 768M). The watch itself (decision 28 on a DEPLOY's first start) is unchanged. | **OPEN — P3; owner: CC (watch)** | — | — | CC | -| **R-762** | Apps & catalog | P3 | **[P2-MEDIUM] wger serves no CSS or JavaScript and no uploaded photo: every static file and every `/media/` file answers 404.** MEASURED 2026-10-01 on 9202 (drill catalog, the live template `82fff32`, wger 2.7), found by checklist rows 1.7 and 2.8: the login page links `/static/css/workout-manager.css`, `/static/bootstrap-compiled.css` — both 404 through traefik; the static root inside the container is empty (4 KB). A progress photo posted to `/api/v2/gallery/` answered 201 and the file is on the media volume, but `GET /media/gallery/…png` answers 404 signed in, without a session, and straight at the app inside the container. **Cause, read in the image:** the entrypoint runs `collectstatic` only when `DJANGO_DEBUG == "False"` and the template sets no `DJANGO_DEBUG`; and wger serves `/media/` only in development (`urls.py:393` „served like this during development only”) — upstream's production setup puts nginx in front for `/static` and `/media`. So the household gets an unstyled app and photos that never show. The same lines stand since the template was written (the 2026-09-29 template too). Not checked: whether any box runs wger (on 2026-09-30 none reported to the hub). **Needs:** `DJANGO_DEBUG=False` (collectstatic) and something that serves `/static` + `/media` (upstream's nginx sidecar, or the gunicorn switch of R-755 plus a static server), proven on the bench and on 9202 with a page that loads its CSS and a photo read back. Owner decides together with R-755 (same server question). `audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt`, `C/C4-seed-photo-size.txt` **-- 2026-10-01 (operator):** wger is `lifecycle: hidden` until this and its twin are fixed (catalog `55b8c8a`; read back on 9202: not on the app list, mealie control present). **Merged 2026-10-05 from R-755 (duplicate):** `templates/wger/docker-compose.yml` still sets no `WGER_USE_GUNICORN` — the gunicorn switch is the same server question. **-- 2026-10-06 (afternoon), measured again on 9202 (live template, wger 2.7):** `/static/css/workout-manager.css` 404 straight at the app, `/home/wger/static` 4 KB, settings `DEBUG False`; the image has gunicorn but no whitenoise and runs Django's `runserver` (no `WGER_USE_GUNICORN`, R-755). So `DJANGO_DEBUG=False` alone would collect the files and still serve none: the fix needs a server for `/static` + `/media` (a second container) — medium, not taken. `audits/r890-instructions-2026-10-06/C/wger.txt`. | **READY — rank P2-MEDIUM; owner: CC (catalog)** **Re-ranked 2026-10-03: P2→P3: wger is hidden from installs and no box runs it; needed only before it is offered again.** **2026-10-06: NARROWED — the files are served.** Catalog `cf1ed43`: `DJANGO_DEBUG=False` + a `wger-files` nginx serving `/static` and `/media`, a definition step proven on the bench (harness v5) and on 9202 (photo 404 → 200, CSS 404 → 200). **What remains is R-755's merged half:** wger still runs Django's `runserver`; upstream's gunicorn runs 3 workers, which do not fit wger's 384 MB — a memory decision and a new proof. Known cost of the fix: the collected static files are 283 MB in a named volume, in every backup of wger. Ready to show? Its files and sign-up are fixed and proven; the server question is open — `audits/design-build-2026-10-06/`F/. **2026-10-06 night: the server question MEASURED on bench 9401** (wger 2.7, template 384M, `wger-files` in front; `audits/night-burndown-2026-10-06/r762/RESULT.md`): runserver anon 201 MB (52 %), gunicorn 1 worker 165 MB (43 %), 2 workers 157 MB (41 % — `--preload` shares pages); oom_kill 0 and restarts 0 in all three; login page and its CSS 200 every time; 20 GETs, 10 at once: avg 394 / 241 / 124 ms. The worker count is gunicorn's `WEB_CONCURRENCY` (the image passes no `-w`). Pick the numbers support: `WGER_USE_GUNICORN=True` + `WEB_CONCURRENCY=2`. Not measured: a signed-in page. Not built: the template change. | — | Decide the gunicorn worker count against the memory limit, prove it on both venues; then the operator decides whether wger is shown. If nothing: wger stays hidden | CC | +| **R-762** | Apps & catalog | P3 | **[P2-MEDIUM] wger serves no CSS or JavaScript and no uploaded photo: every static file and every `/media/` file answers 404.** MEASURED 2026-10-01 on 9202 (drill catalog, the live template `82fff32`, wger 2.7), found by checklist rows 1.7 and 2.8: the login page links `/static/css/workout-manager.css`, `/static/bootstrap-compiled.css` — both 404 through traefik; the static root inside the container is empty (4 KB). A progress photo posted to `/api/v2/gallery/` answered 201 and the file is on the media volume, but `GET /media/gallery/…png` answers 404 signed in, without a session, and straight at the app inside the container. **Cause, read in the image:** the entrypoint runs `collectstatic` only when `DJANGO_DEBUG == "False"` and the template sets no `DJANGO_DEBUG`; and wger serves `/media/` only in development (`urls.py:393` „served like this during development only”) — upstream's production setup puts nginx in front for `/static` and `/media`. So the household gets an unstyled app and photos that never show. The same lines stand since the template was written (the 2026-09-29 template too). Not checked: whether any box runs wger (on 2026-09-30 none reported to the hub). **Needs:** `DJANGO_DEBUG=False` (collectstatic) and something that serves `/static` + `/media` (upstream's nginx sidecar, or the gunicorn switch of R-755 plus a static server), proven on the bench and on 9202 with a page that loads its CSS and a photo read back. Owner decides together with R-755 (same server question). `audits/new-app-checklist-2026-10-01/C/C8-signup-guest-media-static.txt`, `C/C4-seed-photo-size.txt` **-- 2026-10-01 (operator):** wger is `lifecycle: hidden` until this and its twin are fixed (catalog `55b8c8a`; read back on 9202: not on the app list, mealie control present). **Merged 2026-10-05 from R-755 (duplicate):** `templates/wger/docker-compose.yml` still sets no `WGER_USE_GUNICORN` — the gunicorn switch is the same server question. **-- 2026-10-06 (afternoon), measured again on 9202 (live template, wger 2.7):** `/static/css/workout-manager.css` 404 straight at the app, `/home/wger/static` 4 KB, settings `DEBUG False`; the image has gunicorn but no whitenoise and runs Django's `runserver` (no `WGER_USE_GUNICORN`, R-755). So `DJANGO_DEBUG=False` alone would collect the files and still serve none: the fix needs a server for `/static` + `/media` (a second container) — medium, not taken. `audits/r890-instructions-2026-10-06/C/wger.txt`. **-- 2026-10-08 (day):** the gunicorn half PROVEN ON THE BENCH (9401, `wger/server:2.7`, limit 384M): `WGER_USE_GUNICORN=True` + `WEB_CONCURRENCY=2` (the image runs gunicorn only on that switch and passes no `-w`; the container log shows 2 × „Booting worker"); anon peak 166–170 MiB = 43–44 %, oom_kill 0, restarts 0 over a 606 s watch (11,584 requests; harness verdict `proven`); login 200, all three CSS links 200 via `wger-files`, a photo uploaded 201 and read back 200; an unknown `/static/` file 404 (control). **Not done: 9202** — wger is hidden, the live catalog refuses a hidden deploy, and the drill-catalog write that un-hides it there was refused by the permission check (stop, per the brief). **Nothing pushed**; the tested definition is in the evidence. **No ladder step applies:** the change moves no image (from == to), the ladder writer refuses a same-digest re-test, and `09` §5.4 renders the catalog template when the images equal the pinned ones, so the compose change reaches an installed wger at its next `up -d`. Still true: the collected style files (283 MB) go into every wger backup. `audits/day-2026-10-08/r762/`. | **READY — narrowed 2026-10-08: the bench proof is done; the 9202 proof and the push wait for the operator's word on the drill-catalog write.** wger stays `lifecycle: hidden`. | operator (drill-catalog write) | Operator allows the drill-catalog write (fast-forward the drill to live main, un-hide wger there, point 9202 at it) → prove on 9202 (login + CSS + photo 200, memory, 10-min watch) → push the two env lines (no ladder entry) → close | CC | | **R-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: not a catalog fix** — the setgid chain is set by the controller's FileBrowser setup; a controller row. | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC | | **R-577** | Apps & catalog | P4 | **[P3-LOW] A guest SHARE visitor has no way to pick a language, and the household's setting is the wrong default for them.** FOUND 2026-09-18 by localisation slice 2 release C (R-557, controller v0.254.0): every other page a person can reach now carries a language globe — the dashboard (the household's setting), and the sign-in and claim pages (the visitor's own cookie). The two guest share pages (`launcher_shared`, `launcher_share_password`) deliberately do NOT, and `TestGuestSharePagesHaveNoGlobe` pins that so it stays a decision rather than an oversight. **Why it is the operator's and not CC's:** a share visitor is a stranger the household sent a link to, and what language they are shown is a promise the SHARE FEATURE makes, not an implementation detail. The `felhom_lang` cookie already built would fit them exactly (display-only, their own browser, never the household's setting). **Fix shape, if the operator says yes:** add `{{template "lang_globe" .}}` to both shells with the anonymous form, and one render case per page per language. | **READY - rank P3-LOW; owner: operator (the decision), CC (the change)** **Re-ranked 2026-10-03: P3->P4: feature decision for the operator; Hungarian default works today.** | — | — | operator | | **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC | @@ -149,8 +149,8 @@ stopping line that lies. | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| -| **R-232** | Backup & restore | P2 | **DooPlex's backup makes every copy inside the same box — and nothing tells anyone when it fails.** Surveyed read-only 2026-08-06 (`audits/RECON-dooplex-backup-2026-08-06.md`). **What works:** five sets, 14/14 successful runs in 14 days; a file was restored from the `data` repo and matched the live original **byte for byte**; every set except two is cross-disk; k3s is integrity-checked on every run. **What the matrix exposes, ranked:** (a) **`notify_failure` is a no-op** — `NOTIFY_ON_FAILURE=true` but `NOTIFY_WEBHOOK_URL` is commented out, so a failed backup notifies **nobody**; the project already has a working Resend path that CI uses. Cheapest item, and it makes every other failure visible. (b) **Nothing leaves the box** — no rclone, no remote repo, no off-site target anywhere; Longhorn's target is `nfs://192.168.0.180:` pointing at DooPlex itself, and the only outbound-looking cron pulls *inbound* from Hetzner for a different project. The machine that runs the hub managing the customers' off-site chain has no off-site copy of its own. (c) **The backup tree is a single writable path** and the restic repos are not append-only — one bad script or ransomware destroys every copy at once. (d) **Two same-disk sets**: `.claude-memory` and the PostgreSQL dumps, whose source directory sits *inside* the backup tree. (e) **Longhorn `retain=1`** — one generation per volume, so a corruption noticed a day late has no earlier copy. (f) **`/opt/backup/docs/BACKUP-RESTORE.md` does not exist** though the systemd unit advertises it. (g) **`secrets/restic-repo` has never held a snapshot** — `backup-secrets.sh` contains no `restic` call; the secrets are GPG files on `sda1` only. (h) **No restore has ever been run** beyond today's single-file probe — the matrix's "ever demonstrated?" column is otherwise entirely empty. **Not a finding:** the restic passphrase. The on-box copy is on `sdb1`, a different disk from the backups, and the **operator holds an offline copy out of band** — so a disk loss is recoverable. The narrow residual is that it is operator-held rather than system-held, unlike the customer case's hub-vaulted escrow, so it should be confirmed current and findable by someone else. **Nothing was changed by the recon.** | **NARROWED 2026-10-05 — owner Viktor.** (b) partly: the hub database now leaves DooPlex nightly, encrypted, to ep0 (R-173); everything else in DooPlex's backup still stays on the box. (a) partly: the hub copy alarms through Prometheus (`HubDBBackupStale`); `notify_failure` is still a no-op for the rest. (c)–(h) unchanged. **READY** for the rest | — | — | operator | -| **R-304** | Backup & restore | P2 | **The retained escrow key works, and the customer is told their correct code is wrong.** DRILL 2026-08-12 answered the three questions separately, on `demo-felhom`, with planted data. **(a) retention: WORKS** — the first retained row in fleet history to carry material (`host_escrow_superseded` id 11, `identity_blob` 572 B), byte-identical (`sha256 a10032341c8584ed…`) to the pre-supersession `host_escrow` row. **(b) the material opens the old store: YES** — unsealed with the OLD recovery code it yielded a password byte-identical to the pre-change one (`sha c60c8bc737a6b7c6…`), and restored three planted files **byte-identical** from a store the box itself could no longer open (negative control first: `Fatal: wrong password or no key found`), **including a Hungarian accented filename verified as raw bytes**. **(c) the customer's route: DOES NOT EXIST, and misinforms.** `ListSupersededEscrow` (`store.go:2841`) is the only reader of a retained `identity_blob` and has **zero production callers** — five call sites, all `_test.go`; the product path (`POST /escrow/recover-offsite-password` → `FetchIdentityEscrow` → `GetHostDRBundle`, `store.go:3152`) selects `FROM host_escrow` — the CURRENT row only. Asked for the old password with the code that demonstrably opens the retained row, the product answered **"the recovery code did not open the sealed bundle — nothing was written"**. **This is the R-224 class again**: there an unreachable hub was reported as a bad code; here a VALID code for retained history is reported as a bad code, and the customer's attempt ends there. **Consequence:** the census answer stands (it was about retention); the countdown banner's promise is true in substance and false in practice; **any capability-map claim that the customer can recover the old history with their recovery code is false today and must move** | **READY (L) — NEW 2026-08-12, RANK 1** | R-198, R-199, R-224, R-241 | Decide the shape: serve retained rows on the recovery path (needs a "which package?" choice — a customer may have several), or stop promising retrieval anywhere the customer cannot perform it. **Until one of those, the honest position is that retention is an operator-only capability.** At minimum, the refusal must stop asserting the code is wrong when the hub simply never looked | operator + CC | +| **R-232** | Backup & restore | P2 | **DooPlex's backup makes every copy inside the same box — and nothing tells anyone when it fails.** Surveyed read-only 2026-08-06 (`audits/RECON-dooplex-backup-2026-08-06.md`). **What works:** five sets, 14/14 successful runs in 14 days; a file was restored from the `data` repo and matched the live original **byte for byte**; every set except two is cross-disk; k3s is integrity-checked on every run. **What the matrix exposes, ranked:** (a) **`notify_failure` is a no-op** — `NOTIFY_ON_FAILURE=true` but `NOTIFY_WEBHOOK_URL` is commented out, so a failed backup notifies **nobody**; the project already has a working Resend path that CI uses. Cheapest item, and it makes every other failure visible. (b) **Nothing leaves the box** — no rclone, no remote repo, no off-site target anywhere; Longhorn's target is `nfs://192.168.0.180:` pointing at DooPlex itself, and the only outbound-looking cron pulls *inbound* from Hetzner for a different project. The machine that runs the hub managing the customers' off-site chain has no off-site copy of its own. (c) **The backup tree is a single writable path** and the restic repos are not append-only — one bad script or ransomware destroys every copy at once. (d) **Two same-disk sets**: `.claude-memory` and the PostgreSQL dumps, whose source directory sits *inside* the backup tree. (e) **Longhorn `retain=1`** — one generation per volume, so a corruption noticed a day late has no earlier copy. (f) **`/opt/backup/docs/BACKUP-RESTORE.md` does not exist** though the systemd unit advertises it. (g) **`secrets/restic-repo` has never held a snapshot** — `backup-secrets.sh` contains no `restic` call; the secrets are GPG files on `sda1` only. (h) **No restore has ever been run** beyond today's single-file probe — the matrix's "ever demonstrated?" column is otherwise entirely empty. **Not a finding:** the restic passphrase. The on-box copy is on `sdb1`, a different disk from the backups, and the **operator holds an offline copy out of band** — so a disk loss is recoverable. The narrow residual is that it is operator-held rather than system-held, unlike the customer case's hub-vaulted escrow, so it should be confirmed current and findable by someone else. **Nothing was changed by the recon.** | **NARROWED 2026-10-08 — owner Viktor.** (a) DONE with the operator's yes in chat: `notify_failure` now mails admin@felhom.eu through Resend; proven by one test mail that reached the inbox (`audits/day-2026-10-08/r232/`; no backup was started). (b) partly: the hub database leaves DooPlex nightly to ep0 (R-173); everything else stays on the box. (c)–(h) unchanged. **READY** for the rest | — | — | operator | +| **R-304** | Backup & restore | P2 | **The retained escrow key works, and the customer is told their correct code is wrong.** DRILL 2026-08-12 answered the three questions separately, on `demo-felhom`, with planted data. **(a) retention: WORKS** — the first retained row in fleet history to carry material (`host_escrow_superseded` id 11, `identity_blob` 572 B), byte-identical (`sha256 a10032341c8584ed…`) to the pre-supersession `host_escrow` row. **(b) the material opens the old store: YES** — unsealed with the OLD recovery code it yielded a password byte-identical to the pre-change one (`sha c60c8bc737a6b7c6…`), and restored three planted files **byte-identical** from a store the box itself could no longer open (negative control first: `Fatal: wrong password or no key found`), **including a Hungarian accented filename verified as raw bytes**. **(c) the customer's route: DOES NOT EXIST, and misinforms.** `ListSupersededEscrow` (`store.go:2841`) is the only reader of a retained `identity_blob` and has **zero production callers** — five call sites, all `_test.go`; the product path (`POST /escrow/recover-offsite-password` → `FetchIdentityEscrow` → `GetHostDRBundle`, `store.go:3152`) selects `FROM host_escrow` — the CURRENT row only. Asked for the old password with the code that demonstrably opens the retained row, the product answered **"the recovery code did not open the sealed bundle — nothing was written"**. **This is the R-224 class again**: there an unreachable hub was reported as a bad code; here a VALID code for retained history is reported as a bad code, and the customer's attempt ends there. **Consequence:** the census answer stands (it was about retention); the countdown banner's promise is true in substance and false in practice; **any capability-map claim that the customer can recover the old history with their recovery code is false today and must move** | **WAITING-ON-OPERATOR — narrowed 2026-10-08.** Most of the „told the code is wrong" defect closed with R-311 (2026-08-13: the agent tries the retained packages, 422 „correct, earlier package"). The rest fixed on main today: the agent said „wrong code" also when it had NOT tried every earlier package (withheld rows, the caps, a malformed package, an unreadable list) — now 424 `older_unchecked`, and the screen says „we do not know whether your code is wrong … contact support" in Hungarian and English (agent `91b9405`, controller `75b3b39`; ships tomorrow). The route to really serve an old copy is R-312's decision (operator-only); the one-page design and two questions: `audits/day-2026-10-08/design-R-304.md`. | R-198, R-199, R-224, R-241 | Operator answers the two questions in the design (the promise wording; the operator mail, option C) | operator + CC | | **R-518** | Backup & restore | P2 | **[P2-MEDIUM] „Mentés most" on the whole-system backup stops every app for about eight minutes while the page promises „csak néhány másodpercre".** MEASURED 2026-09-14 (BIGNIGHT, VM 333, 12 apps): the button's call quiesced all 12 stacks at 19:03:23Z (first stopped 19:03:27Z); the local vzdump ran 19:03:49 → 19:09:59Z; the controller then kept the apps stopped for the second (PBS) tier and restarted them at 19:10:09Z after it failed, the last started 19:11:12Z (`phase4/guest-backup-quiesce-log.txt`) — **≈ 7 m 45 s** with every app answering 404. The page under the button: „Pillanatkép-mód: az alkalmazások csak néhány másodpercre állnak le." A household pressing it at dinner loses every app for the length of the dump, and longer on a bigger box. **Fix shape:** state the real expected downtime (it scales with data), or quiesce per tier and not across a second tier's attempt; do not start a tier whose storage is absent (see R-517). **NARROWED 2026-09-15 (controller v0.243.0 + agent v0.131.0):** a tier whose storage the agent reports absent is skipped before anything stops (`backup_tier_skipped`, once per absence; unknown never skipped), and the button copy now says „általában néhány perc, nagyobb adatnál több". Unit-proven with red-proofs. **Still open:** quiesce per tier, so a slow second tier does not keep every app down. **— NIGHT 2026-09-23 (controller v0.267.0):** the copy half is DONE: the page and the confirm now state the measured stop (≈ 8 minutes on a 12-app box), both languages, red-proofed (`audits/night-2026-09-23/A5-*`). The brief's „csak néhány másodpercre" had already gone in v0.243.0. **Still open:** quiesce per tier, so a slow second tier does not keep every app down. | **READY — P2, narrowed to per-tier quiesce; owner: CC (controller). 2026-10-05: the copy now states today's measurement too (demo-hp, 9 apps, local tier only: 5 min 47 s) — controller v0.296.0, `audits/hub-safety-2026-10-05/partE/`.** **2026-10-05 (burn-down night): a one-page design proposal (no code) is in `audits/night-burndown-2026-10-05/design-R-518.md`** — for the operator. **2026-10-06: BUILT — controller v0.301.0, `09` §3 decision 156 (reverses R-82's one window).** One stop per tier; the button makes the local copy only. Measured first, read-only: demo-felhom's night off-site job reached `snapshotted` 2 s after it started, the app back 8 s later (the off-site part of a stop is seconds). **Not shown live:** a press under the new rule — scratch 9202 has no agent connection and the demo boxes take deliveries only. Red tests and the build: `audits/design-build-2026-10-06/`D/. **Risk noted, unmeasured:** after a local copy the agent runs its OS step, and the off-site tier then answered BUSY (2026-10-05) — under the new rule that costs one short stop with no copy before the 15-min backoff. **2026-10-06 (night), from Part C:** demo-hp's off-site tier was NOT overdue — its last copy is 2026-10-01 20:15Z (ep0's listing, verify ok), so with the 7-day cadence it is due ~2026-10-08; the night of 2026-10-06→07 is most likely local-only on both demo boxes (demo-felhom's off-site landed 2026-10-06 04:21Z). The two-tier night under the new rule is then ~2026-10-08 on demo-hp. **2026-10-07 (morning): the local-tier night and one press READ BACK** (`audits/readback-2026-10-07/RESULT-B-D.md`): the night stop on demo-hp (9 apps) was ~91 s (was 5 min 47 s), demo-felhom (1 app) ~11 s; one press on demo-hp: 80 s from press to the last app (per app 39–79 s), the copy finished 4 min later with the apps running, only the local tier ran; the page's „kb. 1–1,5 perc" holds. Two channels each (controller log + agent journal / container StartedAt + a 5-s HTTP sampler). **Left:** the first night with both tiers due on demo-hp (~2026-10-08). | — | Read back the ~2026-10-08 night (both tiers on demo-hp); then close | CC | | **R-893** | Backup & restore | P3 | **After a failed OFF-SITE replay, the rollback pours the NEWER pre-restore copy over the OLDER volume just put back.** Read in source 2026-10-06 (R-638 option A, not measured): `internal/backup/offbox_reconstitute.go` writes the undo copy from the live (newer) database, replaces the volumes with the snapshot's older tars, then — when the replay fails — `rollbackSafetyDump` loads that newer dump over the older database volume. The loader only drops what the dump knows, so tables the newer migration removed stay; and when the snapshot's older definition was written, the rollback branch does not put the newer definition back, so the older app starts on rolled-back data; non-database volumes stay at the snapshot's state. An order change cannot fix it (the only undo is a logical dump, and its volume was replaced). Known limit in `07` §6.3. | **OPEN — filed 2026-10-06** **2026-10-06 night: verified in source, no code** — `offbox_reconstitute.go:758` (undo dump from the live DB), `:778-860` (files and volumes from the snapshot), `:804` (the snapshot's definition is written when its version differs), `:898` (the rollback loads the newer dump over the older volume; nothing writes the live definition back). Not a reorder fix: it needs R-638 option B (a rebuilding loader) or a pre-restore volume copy (disk cost; R-685's class). Which state a household gets after a failed off-site replay is the operator's call. Next: the 9202 measurement, then the design. | a design: R-638 option B (a loader that rebuilds instead of overlays) or a pre-restore volume copy | Measure it once on 9202 (a forced replay failure after an off-site restore over a migrated app); then a design for the operator | CC | | **R-895** | Backup & restore | P2 | **The hub's clean-up-window check trusts the snapshot counts the box sends, so a broken-into box (or past-dated fakes added through the add-only key) can shrink the real off-site history without an alarm.** READ 2026-10-06 night in source (R-822's design): the before/after comparison uses counts the box itself reports (`hub/internal/offsitekeys/service.go:284`, `:343`); new fakes keep the count level. Decision 68 already accepts a box-trusted count. | **OPEN — filed 2026-10-06 night** **2026-10-07 07:58: kept open for later (`09` §3 decision 166).** | a design + one read-only measurement (does the Storage Box shell on port 23 show snapshot file upload times?) | Option B of `audits/night-burndown-2026-10-06/design-R-822.md`: the hub lists the repo's `snapshots/` files over its own login before and after a window and alarms on snapshots no box run explains | CC | @@ -215,7 +215,7 @@ stopping line that lies. | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| | **R-836** | Box system & updates | P2 | **A new host kernel that hangs before userspace stays the GRUB default: `--next-boot` is not a one-shot on these hosts.** MEASURED 2026-10-04 on demo-hp (operator's word, 2 reboots): both demo hosts boot UEFI + GRUB without proxmox-boot-tool ESPs; installing a kernel makes it the default at once; `kernel pin --next-boot` writes an ordinary `GRUB_DEFAULT`, and `proxmox-boot-cleanup.service` clears it only after a boot reaches userspace. With the old kernel pinned FIRST, the fallback after a good boot worked (new 60 s, old 76 s). READ FROM THE CODE, not measured: a hang leaves the new kernel default on every power cycle. Only `softdog` runs (useless before userspace); demo-hp's `sp5100_tco` ships unloaded, untested. Fix direction for the slow lane: GRUB's own one-shot (`GRUB_DEFAULT=saved` + `grub-reboot`) with the old kernel saved — to be measured, including Secure Boot (ON on demo-hp). `audits/os-updates-spike-2026-10-04/partH/` | **NARROWED 2026-10-04 (os-host-lane Part E, operator's word before each of 2 reboots) — GRUB's own one-shot is NOT a one-shot here either.** `GRUB_DEFAULT=saved` (old kernel saved) + `grub-reboot `: boot 1 → new kernel, **Secure Boot ON and fine**; but GRUB could not clear `next_entry` (`grub-reboot` itself warns: *environment block on lvm device … will remain the default until manually cleared*; `/boot` is ext4 on LVM `pve-root`), so boot 2 (no command) → **the new kernel again**. `kernel.panic = 0`: a panic leaves the host stopped (R-851). `sp5100_tco` LOADS and answers (`SP5100 TCO timer`, 60 s, inactive, nowayout 0; read from sysfs, never opened, unloaded) — a hardware watchdog exists on demo-hp, but nothing arms it before userspace. demo-hp left on 7.0.14-20 with that as the saved default. **LEFT (fix direction):** a GRUB env block GRUB can write (on the ESP, vfat) or a userspace "boot good" step that rewrites the default, plus arming `sp5100_tco`; to be measured before the kernel slow lane. `audits/os-host-lane-2026-10-04/partE/` **READY — owner: CC + operator (reboots).** **2026-10-07 07:58: raised P3 → P2 (`09` §3 decision 164 — the kernel lane before the first paying customer); the spike runs with the operator present.** **2026-10-07 SPIKE DONE (operator present; 24 reboots, 2 power cycles; `audits/kernel-spike-2026-10-07/DESIGN-kernel-lane.md`).** On Tester 1 (VM), demo-felhom and demo-hp (Secure Boot on): **a one-shot flag in a GRUB env block on the ESP (vfat) WORKS** — new kernel once, then the old one; a panicking new kernel (`panic=10`) falls back with no person. **UEFI `BootNext` WORKS too.** **A hardware watchdog armed by systemd during the reboot FAILS on all three** (i6300ESB, Intel TCO, AMD SP5100): the reset clears the timer, so a FROZEN new kernel needs a power cycle. Pick: the ESP flag + lockup-to-panic kernel options (unmeasured). Two operator questions in the design (night restarts and the household; a freeze needs a person). Boxes left clean on a healthily booted default. **2026-10-07 14:43 — `09` §3 decision 172: the kernel lane is YES — option A (the ESP one-shot flag) with option C (lockup → panic) on the one-shot entry; a box may restart at night; the household is mailed the day before (what to do if it is not back: unplug, plug back in); each kernel set is operator-approved after ring 0; a frozen new kernel needing a power cycle is ACCEPTED for the first customers. BUILD STARTED 2026-10-07 (the kernel-lane arc).** **2026-10-07 evening: BUILT and proven by hand** — agent v0.152.0 + hub v0.143.0 (`11` §5.11), delivered to demo-hp, demo-felhom and Tester 1. Tester 1 (`audits/kernel-lane-2026-10-07/E/RESULT.md`): a forced panic on the one-shot → back on the old kernel by itself (fell_back, 0 unclean boots counted); a held guest → judged 20 min → ONE self-revert → old kernel; a healthy boot → 7.0.14-22 the default. The household mail went out for demo-felhom 18:12 (Gmail). Option C UNMEASURED (no test_lockup module). **WHERE IT STOPPED (next session):** the ring-0 NIGHT run (Part E3) — tonight demo-felhom refuses the told 7.0.14-20 (R-898, safe); 7.0.14-22 is due on both demo boxes for the night 2026-10-08→09 (mail 2026-10-08 daytime); read back the morning of 2026-10-09; then approve the set on the System page and deliver to the Tester 1 box as ring 1 by a signed os_kernel_step (Part E4 — Tester 1 already runs 7.0.14-22, so the ring-1 proof needs the NEXT kernel). **2026-10-07 evening (decision 176): the night run moved to TONIGHT 7→8.** Pre-night fixes delivered (agent v0.153.0 — ring 0 stages exactly the told kernel, R-898 closed; controller v0.303.0 — the first capture after a boot waits for the drive, R-897 closed; hub v0.143.1 — Reply-To on household mails). Armed at 19:05: demo-felhom due 7.0.14-20 (household told 18:12), demo-hp due 7.0.14-22 (told 18:38; demo-hp's day-old apt lists refreshed by a report-only pass with its updates switched off for ~1 min). **WHERE IT STOPPED: read back the night on 2026-10-08 morning** (each box on its kernel as default, the apps back, minutes down, no false backup failure), then approve the set (the two boxes ran DIFFERENT kernels — the button waits until both boot the same one) and Part E4. **2026-10-08 read-back (`audits/kernel-night-2026-10-07/readback/`): demo-felhom PASSED its first real kernel night** — whole-guest backup 04:35, OS + Proxmox steps 04:37–04:38, kernel staged 04:39:04 as EXACTLY the told 7.0.14-20 (not the newer 22 — R-898's fix held), restart 04:39:08, new kernel judged healthy 04:40:38 (38 s after the agent started), now the default; apps away ~1.5 min; no operator alarm, no false backup failure (demo-felhom has no drive apps, so R-897's fix was not exercised there); crash guard 0. **demo-hp took NO step** — no whole-guest backup that night (R-899): it stays on 7.0.14-20, still due 7.0.14-22; a new household mail is allowed after 14:38 today, so it should run the night 2026-10-08→09. Reply-To proven: the operator's reply to the test mail went to admin@felhom.eu (Gmail 2026-10-07 18:29 UTC). **WHERE IT STOPPED:** read back demo-hp the morning of 2026-10-09; the approval button waits until both demo boxes booted the SAME kernel (now 20 vs due 22) — likely a step to 22 on demo-felhom later; then Part E4. | — | — | CC | -| **R-899** | Box system & updates | P3 | **A daytime whole-guest backup moves the 24 h cadence past the night window, so the next night has no whole-guest backup — and with it no OS leg and no kernel step.** SEEN 2026-10-08: demo-hp's last whole-guest backup was a morning press at 2026-10-07 08:49:53; due again 2026-10-08 08:49, after the window [04:30, 08:30) closed → no backup, no night leg; the household had been told „tonight" (18:38) and nothing happened (harmless: no change). The kernel lane recovers by itself (a new mail after 20 h, step the next night, inside the 3-mail limit), but every daytime press costs one night. Fix direction (a design, not taken): the cadence counts nights, not 24 h (due when the last whole-guest backup is older than ~20 h at the window's start), or a press does not reset the night's cadence. `audits/kernel-night-2026-10-07/readback/demo-hp-why-no-step.txt` | **OPEN — filed 2026-10-08 (kernel-night read-back).** | — | — | CC | +| **R-899** | Box system & updates | P3 | **A daytime whole-guest backup moves the 24 h cadence past the night window, so the next night has no whole-guest backup — and with it no OS leg and no kernel step.** SEEN 2026-10-08: demo-hp's last whole-guest backup was a morning press at 2026-10-07 08:49:53; due again 2026-10-08 08:49, after the window [04:30, 08:30) closed → no backup, no night leg; the household had been told „tonight" (18:38) and nothing happened (harmless: no change). The kernel lane recovers by itself (a new mail after 20 h, step the next night, inside the 3-mail limit), but every daytime press costs one night. Fix direction (a design, not taken): the cadence counts nights, not 24 h (due when the last whole-guest backup is older than ~20 h at the window's start), or a press does not reset the night's cadence. `audits/kernel-night-2026-10-07/readback/demo-hp-why-no-step.txt` | **VERIFY — fixed on main 2026-10-08, ships with tomorrow's controller + agent releases** (operator ruling 2026-10-08 07:12, option A, `09` §3 decision 177: every night takes its own). Controller `6d07ca2`: a durable ledger (last successful press / last successful scheduled run per tier); a tier the agent calls „not due" because of a press is still due inside tonight's window, never by the safety valve (`TestR899_*`, red-proved). Agent `4c69c25`: a press arrives as `trigger=manual` and runs no OS leg; the after-boot kernel reports carry the saved ring instead of „ring 1". The household mail case („tonight" for a night whose backup cannot start because of a press) is removed by the rule; no hub change. `07` §6.1. | — | Release controller + agent tomorrow; read back the first night after a daytime press; then close | CC | | **R-862** | Box system & updates | P3 | **Tester 2 cannot take the config bundle until one by-hand bootstrap is done: its `felhom-os-apply` (agent 0.142.0) predates the bundle mode, and no signed job can write a root file on it.** FOUND 2026-10-04 (R-840 build, Part C): the route reaches every box installed from installer 1.31.0 on, and the demo boxes (bootstrapped by CC); Tester 2 has every root file of agent 0.142.0 (installer 1.30.0) and lacks only the R-858 wrapper fix, which matters only for a Docker step it gets solely from a signed job. CC has no route to Tester 2 (its door admits only the operator's WireGuard peer; `felhom-op` cannot become root). The steps: `runbooks/config-bundle.md` "Tester 2". Then CC signs the bundle and reads it back. | **WAITING-ON-OPERATOR** — the operator said (2026-10-04 ~19:05) he will try through his tunnel **2026-10-07 07:58 (`09` §3 decision 170): the operator believes Tester 2 never set up the off-site backup (no escrow); the laptop may stay off for days; the tester plans to add an HDD.** **2026-10-07 (Part H, read only):** in Tester 2's last 10 notifications (2026-10-05 09:16 → 2026-10-07 06:58) the household got 0 mails, the operator 10 (host_down ×9, expected_dbdump_missed ×1); control: the household channel shows on the other three customers. So the household is not mailed daily while the box is off; no row (`audits/day-2026-10-07/H/`). | the operator's WireGuard tunnel | the operator runs the three bootstrap commands; CC sends the bundle | operator | | **R-35** | Box system & updates | P3 | **Config-apply should not end the customer's session.** The offsite config push bumped `config_version` 10→11 at 16:54:58 and the controller self-restarted (container `StartedAt` 16:54:59Z, back up 16:55:02); in-memory sessions died with it and **customer zero was force-logged-out mid-flow**. **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-21, size S, roadmap state `idea`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-05 (burn-down night): NEEDS A DESIGN.** Hot-apply needs a per-setting ruling; persisting sessions puts login tokens on disk (and into the whole-guest archive, `07` §5). Next: the operator's pick between them. | — | Direction: **hot-apply the offbox target** (no restart for a config the running process can adopt), or **persist sessions** across restart. The restart itself is by design — the collateral is not. Evidence `controller-log-full.txt` | CC | | **R-78** | Box system & updates | P3 | **`local_api` authority ruling — auto-reconcile vs detect-only** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-25, size M, roadmap state `idea (deferred OUT of R-77 on purpose)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. **An OWED OPERATOR DECISION, not a defect — moved because an owed ruling hidden among feature ideas is the shape this session exists to remove.** | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** | — | R-77 ships detection because the fix is genuinely undecided, and **both directions can lose customer-visible function**. **Direction 1 (today):** `controller.yaml` wins and drift is silent → the 2026-07-25 island migration blinded the whole fleet's control plane for 17.5 h (drive gate, guest-reboot recovery, quiesce/backup all degrade). R-77 makes that loud but does not stop it recurring. **Direction 2 (`bootstrap.json` wins, auto-reconcile on boot):** a guest whose `controller.yaml` is CORRECT and whose `bootstrap.json` is stale — a half-completed re-provision, a hand-repaired guest, a setup-wizard box — gets a **working channel clobbered on the next restart**, fleet-wide and silently, during a routine deploy. That is not obviously better than the bug. Needs a spike: which writer is authoritative per field (endpoint vs fingerprint vs token — `mergeLocalAPI` replaces the whole block, so they cannot be reconciled independently today), whether the agent side should stamp a generation/mtime so 'newer wins' is even expressible, and whether reconcile should require an operator ack. Until then the drift alert plus a manual edit is the supported path. | CC | @@ -226,7 +226,7 @@ stopping line that lies. | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| -| **R-243** | Monitoring & notifications | P2 | **A box in the R-241 state silently stops backing up off-site, and NO ALARM OF ANY KIND FIRES.** Found by the R-241 spike (2026-08-07) as a by-product; **not part of the walk's finding and not previously filed.** The R-241 state is self-locking in a second, worse way than the recovery-journey dead end: `escrow_state` is stuck `pending` forever (the auto-confirm flips only on a hash match, and the hash cannot match a key the box minted itself), and `runOffboxBackup` returns at the escrow gate (`offbox.go:743`) before touching anything. **So off-site backups never run again — and the hub never notices.** All three signals that could catch it are excluded, each for its own individually-correct reason, verified in the hub this session: `offsite_stale` — `isStale` (`monitor/offsite.go:135`) returns false unless `EscrowState == "escrowed"`, and its own comment reads *"Pending/disabled = normal onboarding, never stale"*, so the box is classified as **still being set up, forever**; `offsite_delivery_stuck` — `monitor/offsite_delivery.go:91` skips the `applied` shape, and delivery genuinely IS applied (the credential was consumed and the target is in every report); `backup_failed` — never fires, because nothing fails: the run returns `nil` before it starts. **Three correct exclusions leaving one state unobserved.** This is the same class as the workspace `CLAUDE.md` "presence is not success" rule, one level up: **the absence of a failure is being read as the presence of a working tier.** **Partly subsumed by R-241's fix** — a box that recovers leaves this state — but **not for a box that does not**, and the alarm gap is what makes "does not" survivable indefinitely. **Not fixed; no code written.** **⚠ UPDATED 2026-08-07 (v0.206.0) — the STATE this row describes can no longer be entered, but the ALARM GAP is untouched and the row stays open.** R-241's mint guard means a box no longer mints a key over a sealed package, so it no longer arrives in the "escrow stuck pending against a self-minted key" state by itself. **What replaces it is a state that is VISIBLE rather than silent:** the box declares `offsite.state=awaiting_recovery_key` and the customer is offered the recovery screen. **But the hub still raises nothing for it**, and for the same three reasons: `isStale` needs `escrowed`, the delivery checker skips the `applied` shape, and `backup_failed` needs a run that never happens. **So a box whose customer never acts still stops backing up off-site with no operator signal** — the difference is that the customer can now see it and act, where before nobody could. **The remaining work is an operator-side signal for a box held in `awaiting_recovery_key` past some age**, and it is deliberately not bundled into R-241's fix. **⚠ MEASURED ON A REBUILD, 2026-08-07 (fifth walk) — the gap is real for the state this row describes, and NOT for the state a rebuild produces.** 88 seconds after the walk5 guest was destroyed and rebuilt, the hub emitted `offsite_delivery_stuck` (**warning**) and wrote an **operator-channel** `notification_log` row recording `offsite_credential_restaged` / status **REFUSED** with an accurate reason — *"the credential was applied and worked; the target was lost afterwards … a guest rebuild does, R-193"*. So on the **regressed-apply** shape the operator IS told, promptly and correctly, and this row's *"skips the applied shape"* does not apply. The gap stands for a box that reaches the held state **without** a prior working tier in its report history. **Recorded so the row is not read wider than it measures.** | **READY** — owner Viktor | — | — | operator | +| **R-243** | Monitoring & notifications | P2 | **A box in the R-241 state silently stops backing up off-site, and NO ALARM OF ANY KIND FIRES.** Found by the R-241 spike (2026-08-07) as a by-product; **not part of the walk's finding and not previously filed.** The R-241 state is self-locking in a second, worse way than the recovery-journey dead end: `escrow_state` is stuck `pending` forever (the auto-confirm flips only on a hash match, and the hash cannot match a key the box minted itself), and `runOffboxBackup` returns at the escrow gate (`offbox.go:743`) before touching anything. **So off-site backups never run again — and the hub never notices.** All three signals that could catch it are excluded, each for its own individually-correct reason, verified in the hub this session: `offsite_stale` — `isStale` (`monitor/offsite.go:135`) returns false unless `EscrowState == "escrowed"`, and its own comment reads *"Pending/disabled = normal onboarding, never stale"*, so the box is classified as **still being set up, forever**; `offsite_delivery_stuck` — `monitor/offsite_delivery.go:91` skips the `applied` shape, and delivery genuinely IS applied (the credential was consumed and the target is in every report); `backup_failed` — never fires, because nothing fails: the run returns `nil` before it starts. **Three correct exclusions leaving one state unobserved.** This is the same class as the workspace `CLAUDE.md` "presence is not success" rule, one level up: **the absence of a failure is being read as the presence of a working tier.** **Partly subsumed by R-241's fix** — a box that recovers leaves this state — but **not for a box that does not**, and the alarm gap is what makes "does not" survivable indefinitely. **Not fixed; no code written.** **⚠ UPDATED 2026-08-07 (v0.206.0) — the STATE this row describes can no longer be entered, but the ALARM GAP is untouched and the row stays open.** R-241's mint guard means a box no longer mints a key over a sealed package, so it no longer arrives in the "escrow stuck pending against a self-minted key" state by itself. **What replaces it is a state that is VISIBLE rather than silent:** the box declares `offsite.state=awaiting_recovery_key` and the customer is offered the recovery screen. **But the hub still raises nothing for it**, and for the same three reasons: `isStale` needs `escrowed`, the delivery checker skips the `applied` shape, and `backup_failed` needs a run that never happens. **So a box whose customer never acts still stops backing up off-site with no operator signal** — the difference is that the customer can now see it and act, where before nobody could. **The remaining work is an operator-side signal for a box held in `awaiting_recovery_key` past some age**, and it is deliberately not bundled into R-241's fix. **⚠ MEASURED ON A REBUILD, 2026-08-07 (fifth walk) — the gap is real for the state this row describes, and NOT for the state a rebuild produces.** 88 seconds after the walk5 guest was destroyed and rebuilt, the hub emitted `offsite_delivery_stuck` (**warning**) and wrote an **operator-channel** `notification_log` row recording `offsite_credential_restaged` / status **REFUSED** with an accurate reason — *"the credential was applied and worked; the target was lost afterwards … a guest rebuild does, R-193"*. So on the **regressed-apply** shape the operator IS told, promptly and correctly, and this row's *"skips the applied shape"* does not apply. The gap stands for a box that reaches the held state **without** a prior working tier in its report history. **Recorded so the row is not read wider than it measures.** | **VERIFY — built on hub main 2026-10-08 (`b119301c`), ships with tomorrow's hub release.** New operator-only `offsite_escrow_pending` (warning): off-site ON, escrow not done, no successful off-site run for 7 days (CC picked 7 days, `09` §3 decision 179 — operator may reverse); weekly re-send, persisted, clears with an info line. `08` §6.3. **Expected to fire for Tester 2 on the first sweep after the deploy** (hub read 2026-10-08: off-site on, escrow `pending`, never a success). | — | Deploy hub tomorrow; confirm the Tester 2 mail arrives (positive control); then close | operator | | **R-528** | Monitoring & notifications | P2 | **[P2-MEDIUM] Docker does not report an OOM kill inside a Felhom LXC guest: `OOMKilled` stays false and no `oom` event fires, so the v0.243.0 OOM line is not proven live.** MEASURED 2026-09-15 on scratch 9202 (Docker 29.8.0): Paperless capped at 128M restarted 11 times with `OOMKilled=false` and zero `docker events --filter event=oom`; a memory hog inside the running container was killed (rc 137) with the same silence (`E2-oom-signal-measure-9202.txt`). BIGNIGHT VM 333 did read `oomkilled=true`, so the shape differs by case. **Fix shape:** the agent reads the guest container cgroups' `memory.events oom_kill` counters (host-side, reliable), or the controller alarms on a restart-count trend **RE-MEASURED 2026-09-16 on the DRILL box (fresh install, nested VM 334, Docker in an LXC guest, controller 0.243.0), so the finding is not a property of one machine:** the Paperless webserver was capped at 128 M with `docker update --memory`; it restarted 9-10 times, and all three signals stayed silent - `OOMKilled=false` on every inspect, `docker events --filter event=oom` EMPTY for the whole window, the container's cgroup not visible from inside the guest, and `dmesg` unreadable there. Identical to scratch 9202. So the v0.243.0 OOM line cannot fire on ANY Felhom box as shipped, on either host. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-m1-oom.txt`. | **READY — rank P2-MEDIUM; owner: CC** **2026-09-17 (chaos night): an OOM WAS detected on a fresh box, and named precisely.** On `tester-1-022354` (controller 0.245.0, guest 9201, 6 GB RAM) immich’s Postgres was killed by the memory limit during its reverse-geocoding import, and the controller pushed `app_oom` (warning, operator-only): „Alkalmazás memóriája elfogyott: immich (immich-postgres) — egy folyamatát a memóriakorlát leállította” — naming the app AND the exact container. The visible consequence was `write CONNECTION_CLOSED immich-postgres:5432` and twelve restarts of immich-server. So on THIS box the OOM scan works and was the fastest route to the diagnosis; recorded here rather than filed as a new row. Evidence: `audits/evidence-chaos-night-2026-09-17/round-2.txt`. **2026-10-05 (burn-down night): a one-page design proposal (no code) is in `audits/night-burndown-2026-10-05/design-R-528.md`** — for the operator. **2026-10-06: STOPPED BEFORE ANY CODE — the measurement contradicted the design** (decision 155 said A then C). On scratch 9202, Docker 29.8.2, the `OOMKilled` flag was TRUE in all four shapes tried: a child process killed while the container kept running (`oom_kill` 0 → 3, flag true) and three main-process kills (exit 137, flag true); an `oom` event each time. The false flags of 2026-09-15 were on Docker 29.8.0. Cost read for the record: one exec read of 21 containers on demo-hp = 1.6 s. `audits/design-build-2026-10-06/`C/. **2026-10-06 18:24: operator ruling, option A (decision 157):** nothing is built; the row stays open as a WATCH for a box whose Docker reports a false flag. **Added:** a Docker engine set may not be approved until it reports a memory kill correctly on the boxes that ran it (built in this row's next session). **2026-10-06 (night): the Docker-approval memory-kill check is BUILT** — hub v0.140.0 (LIVE: the approval waits for a passing `oom_check` on every ring-0 box; a failed, errored or missing one blocks) and the agent's wrapper (`felhom-agent` `acccb66`, unreleased — ships as v0.150.0 after the 2026-10-07 read-back). Proven by hand on demo-hp's guest: `OOMKilled=true`, exit 137, the `oom` event — seen only with an events window ending after the run (the wrapper waits 2 s). `audits/readback-2026-10-07/`F/. | — | Release agent v0.150.0 + bundle and deliver it; then the row is a WATCH only | CC | | **R-79** | Monitoring & notifications | P3 | **`report.Issues` / `report.Warnings` are English on customer-facing surfaces** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size M, roadmap state `idea`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-05 (burn-down night): NEEDS A DESIGN** — the issues and warnings travel to the hub as sentences; changing them is the two-repo spike the row itself names. | — | **Whole-surface, not a one-off** (DIAG §6): every producer is English — `"SSD/HDD disk usage critical"`, `"Docker: %v"`, `"Protected container not running: %s"`, and all six `Warnings` strings. They render on the customer's Hungarian dashboard, and the `health_critical` path has reached the **customer** email channel three times historically. Deliberately NOT bundled into R-77: a copy sweep across every producer would have buried two safety fixes in string churn, and the seam is not obvious — translate at the producer, or at the render/notification boundary where operator-English and customer-Hungarian already diverge? Pick the seam in a spike; the strings are mechanical after. | CC | | **R-211** | Monitoring & notifications | P3 | **Prometheus has no config-reloader — a rules change reaches the pod and is never read** | **READY (S) — NEW 2026-08-05** | — | Found while verifying R-205 rather than by looking for it. The `mon-system/prometheus` Deployment runs **one** container (`prom/prometheus:v3.12.0`) with **no `configmap-reload`/`prometheus-config-reloader` sidecar**. After the ArgoCD sync the updated `node-housekeeping-alerts.yml` was present **inside the pod** (`grep -c "and on(instance)"` → 3 on the mounted symlink) while the Prometheus **rules API still served the old expression** — for **4+ minutes**, with no error anywhere. It only took effect after an explicit `POST /-/reload`. **The consequence is general, not specific to R-205: every rule edit in this repo since the stack was built has silently not applied until something happened to restart the pod** — so "committed and synced" has never meant "in force", and ArgoCD reporting `Synced/Healthy` is true and beside the point. `--web.enable-lifecycle` IS already set, so the fix is small: add a reloader sidecar watching the ConfigMap, or a `checksum/config` pod annotation so a rules change rolls the pod. **Same class as the four *built-but-never-wired* seams** — the control exists, nothing walks it | CC | @@ -266,7 +266,9 @@ stopping line that lies. | **R-784** | Business & legal | P2 | **[P2-MEDIUM] SparkyFitness's licence forbids commercial use: "may not be used, directly or indirectly, in any product, service … intended for … commercial advantage … without prior written permission from the author" — and Felhom is a paid service that offers it in its catalog.** READ 2026-10-01 (`audits/visitors-2026-10-01/C/C0-license.txt`): a custom licence (GitHub: NOASSERTION), the same at the pinned tag v0.17.3 and at `main`; termination clause 7 ("cease all use"). SparkyFitness was named as wger's replacement for fitness. **Needs (operator):** (A) hide it from new installs (`lifecycle: hidden`) until the author gives written permission, and ask; (B) ask first and keep it offered meanwhile; (C) keep it. Recommended A. If nothing is decided it stays offered. **UPDATE 2026-10-02 — DECIDED (`09` §3 decision 65, option B):** SparkyFitness stays offered; the operator asks the author. The request is drafted (NOT sent): `audits/licences-2026-10-02/EMAIL-DRAFT-sparkyfitness.md` — the author publishes no e-mail; the routes are the project's Discord (private, recommended) or a GitHub Discussion. **Trigger:** no written permission before the first paying customer → `lifecycle: hidden` (a STATUS standing item). **2026-10-08: the public apps page lists SparkyFitness** (Hungarian and English), badged *Korlátozott licenc* / *Restricted licence*, as it lists Tandoor (R-789) — the box offers both today; removing the cards is one small commit if the decision goes that way. | **WAITING-ON-OPERATOR — rank P2-MEDIUM; owner: operator (send the request; the answer)** | — | — | operator | | **R-789** | Business & legal | P2 | **[P2-MEDIUM] Tandoor's licence is AGPL-3.0 WITH the Commons Clause: it forbids selling "a product or service whose value derives, entirely or substantially, from the functionality of the Software" — fees for hosting or support included.** READ 2026-10-02 at 2.6.15 (`audits/licences-2026-10-02/TABLE.md`). Felhom charges for installing and caring for the household's apps; whether that value comes "substantially" from Tandoor is the question. **Needs (operator):** keep (the fee is for the box, not Tandoor), hide for new installs, or ask the authors. Nothing changed meanwhile. **RULED 2026-10-02 afternoon (`09` §3 decision 66):** treated like SparkyFitness — stays offered; the operator asks the authors for written permission; without it before the first paying customer Tandoor is hidden (`lifecycle: hidden`). STATUS "Before the first paying customer". | **WAITING-ON-OPERATOR — rank P2-MEDIUM; owner: operator (the request; trigger: first paying customer)** | — | — | operator | | **R-802** | Business & legal | P2 | **[P2-MEDIUM] A lawyer reviews the non-OSI licence list before the first paying customer.** Operator ruling 2026-10-02 (`09` §3 decision 66): Tandoor (R-789), SparkyFitness (R-784), Emby, n8n, Plex (kept — R-790..R-792), the EE/BUSL parts (R-793), redis 7.4 (R-794); the table is `audits/licences-2026-10-02/TABLE.md`. STATUS "Before the first paying customer". | **WAITING-ON-OPERATOR — rank P2-MEDIUM; owner: operator (trigger: first paying customer)** | — | — | operator | -| **R-813** | Business & legal | P2 | **[P2] The website collects personal data but publishes no privacy notice, no terms and no imprint.** CHECKED 2026-10-03 (read-only): `website/` holds nine Hungarian pages and one English page; none is an ÁSZF, an adatkezelési tájékoztató or an impresszum, and no page links to one (ASCII-fragment search `aszf`, `adatkezel`, `impresszum`, `impressum`, `privacy` over `website/`; positive control: the same search finds `adatkezel` in the contact form). The contact form makes the visitor tick a data-processing consent (`website/kapcsolat.html:123-128`) whose text names no controller, no retention and no rights, and links nowhere. The papers around it — contract, data-processing agreement, billing — are the intention **R-809** in `ROADMAP.md`. **2026-10-08 (website refresh): the English twins are PUBLIC since today** (`felhom.eu/en/…`, operator choice B) — **the legal pages are needed in English too**, or an English line saying the legal texts are Hungarian, linked from every English page. The English contact form translates the consent text as it is. The refresh CUT the FAQ's „önálló modell" (no row backs it, brief Part A) and LEFT the GDPR answer („nem harmadik félnél") for R-900; the English FAQ carries the same answer. | **WAITING-ON-OPERATOR — the operator writes or commissions the texts; CC drafts on request; owner: operator** | — | — | operator | +| **R-813** | Business & legal | P2 | **[P2] The website collects personal data but publishes no privacy notice, no terms and no imprint.** CHECKED 2026-10-03 (read-only): `website/` holds nine Hungarian pages and one English page; none is an ÁSZF, an adatkezelési tájékoztató or an impresszum, and no page links to one (ASCII-fragment search `aszf`, `adatkezel`, `impresszum`, `impressum`, `privacy` over `website/`; positive control: the same search finds `adatkezel` in the contact form). The contact form makes the visitor tick a data-processing consent (`website/kapcsolat.html:123-128`) whose text names no controller, no retention and no rights, and links nowhere. The papers around it — contract, data-processing agreement, billing — are the intention **R-809** in `ROADMAP.md`. **2026-10-08 (website refresh): the English twins are PUBLIC since today** (`felhom.eu/en/…`, operator choice B) — **the legal pages are needed in English too**, or an English line saying the legal texts are Hungarian, linked from every English page. The English contact form translates the consent text as it is. The refresh CUT the FAQ's „önálló modell" (no row backs it, brief Part A) and LEFT the GDPR answer („nem harmadik félnél") for R-900; the English FAQ carries the same answer. | **WAITING-ON-OPERATOR — the operator writes or commissions the texts; CC drafts on request; owner: operator** **2026-10-08: first drafts written (not published, pending lawyer review R-802): `documentation/legal/` — `DRAFT-aszf.md`, `DRAFT-adatkezelesi-tajekoztato.md`, `DRAFT-impresszum.md`, `DRAFT-kapcsolat-hozzajarulas.md` (proposed consent text + link). Every fact the operator supplies is a `[[…]]` placeholder; the lawyer list and the guesses are at the end of each draft. Owner stays operator.** | — | — | operator | +| **R-900** | Business & legal | P2 | **The website's FAQ says the household's data is not with a third party, while the system sends copies and traffic to processors.** FOUND 2026-10-08 while drafting the privacy notice (R-813): `website/gyik.html` (GDPR answer, „az adataid a saját infrastruktúrádon vannak — nem harmadik félnél") — but the encrypted off-site copies go to Hetzner (Storage Box, ep0), app traffic passes Cloudflare's tunnel (TLS ends at its edge), box reports and on-request log tails go to the hub, the hub's mails go through Resend. The drafting session also read an offer of an „önálló modell" (remote access removed) in the FAQ; after the same day's website refresh that phrase no longer appears (ASCII and accented search, 0 hits) — re-check the refreshed text against `07` §2 (operator root access is part of the product). A promise the product makes is the operator's to reword. Lawyer list and processor table: `documentation/legal/DRAFT-adatkezelesi-tajekoztato.md` §11. | **WAITING-ON-OPERATOR — owner: operator** | R-813 | Reword the FAQ answers to match the privacy notice (or decide the product changes); not changed by CC — a promise to customers | operator | +| **R-901** | Business & legal | P2 | **Two kinds of a household's data outlive the deletion of the customer, and no document says when they go.** FOUND 2026-10-08 (R-813 drafting), read in source: the hub keeps `events` and `notification_log` on purpose after a customer is deleted (`hub/internal/store/customer_delete.go:24-26`, „the audit trail outlives every lifecycle tier") and nothing prunes `notification_log`; DooPlex's copy of ep0 (`ep0-copy`) pulls with `remove-vanished false` and prunes only to keep-weekly 8 (`runbooks/ep0-datastore-copy.md`), so a deleted customer's last 8 weekly encrypted whole-guest copies stay on DooPlex with no end date. A privacy notice cannot promise deletion until this is decided. | **WAITING-ON-OPERATOR — owner: operator** | R-813 | Decide the retention after deletion (audit rows: how long; ep0-copy: remove a deleted customer's namespace, by hand or by job) and write it into the privacy notice | operator | | **R-89** | Business & legal | P4 | Retention as a per-customer **commercial** policy on the hub | READY (increment 2) | — | Policy object + reconciler → ep0 prune job; keep box tokens write-only | CC | | **R-794** | Business & legal | P4 | **[P3-LOW] redis 7.4 (RSALv2 / SSPL, not OSI) runs as a private cache in seven apps: dawarich, docmost, immich, nextcloud, outline, paperless-ngx, romm.** READ 2026-10-02 (`audits/licences-2026-10-02/TABLE.md`). Read as permitted (a private cache only its app uses is not Redis offered as a service — inferred). Valkey (BSD-3) or redis 8 (AGPL option) removes the question. **Needs:** a ladder step per app to valkey or redis 8, through the harness — no hurry. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: the row itself says no hurry; usage read as permitted.** | — | — | CC | diff --git a/documentation/legal/DRAFT-adatkezelesi-tajekoztato.md b/documentation/legal/DRAFT-adatkezelesi-tajekoztato.md new file mode 100644 index 00000000..19b0ef01 --- /dev/null +++ b/documentation/legal/DRAFT-adatkezelesi-tajekoztato.md @@ -0,0 +1,271 @@ +# DRAFT — Adatkezelési tájékoztató (Felhom) + +> **English summary.** First draft of the Felhom privacy notice, written 2026-10-08 for R-813. NOT +> published, NOT legal advice, pending lawyer review (R-802). It lists what personal data Felhom +> handles, where, by whom and for how long — built from the architecture documents and the code, +> not from a template; every line cites its source in an HTML comment. Operator facts are +> placeholders (`[[CÉGNÉV]]` etc.); facts the system's documents do not state are `[[ELLENŐRIZNI]]`. +> Every legal basis named is a candidate for the lawyer, not a conclusion. Section 12 lists what the +> lawyer must check and every point where the draft guessed. Section 11 lists places where the +> website today says something the system does not do. + +--- + +**Hatály:** [[HATÁLYBALÉPÉS DÁTUMA]] · **Verzió:** [[VERZIÓ]] + +## 1. Ki az adatkezelő? + +| | | +|---|---| +| Név | [[CÉGNÉV]] | +| Székhely | [[SZÉKHELY]] | +| Cégjegyzékszám / nyilvántartási szám | [[CÉGJEGYZÉKSZÁM]] | +| Adószám | [[ADÓSZÁM]] | +| E-mail | [[ADATVÉDELMI E-MAIL]] | +| Telefon | [[TELEFON]] | +| Adatvédelmi tisztviselő | [[ADATVÉDELMI TISZTVISELŐ — vagy: nincs kijelölve]] | + +A továbbiakban: **Felhom** vagy **mi**. + +## 2. Kétféle szerepünk van — és ez a tájékoztató csak az egyikről szól + +1. **Adatkezelőként** kezeljük: a weboldal látogatóinak és a kapcsolatfelvételi űrlap kitöltőinek + adatait, valamint az előfizetők (háztartások) ügyfél- és szerződéses adatait. Erről szól ez a + tájékoztató. +2. **Adatfeldolgozóként** járunk el a háztartás saját szerverén tárolt adatok (fényképek, + dokumentumok, alkalmazásadatok) tekintetében: a szervert üzemeltetjük, felügyeljük és mentjük, de + az adatok a háztartáséi. Ennek feltételeit a [[ADATFELDOLGOZÁSI MEGÁLLAPODÁS — hivatkozás]] + rögzíti. Ahol ez a tájékoztató a szerveren lévő adatokat érinti (mentés, hozzáférés), azt + átláthatósági okból írjuk le. + + +## 3. A weboldal (felhom.eu) + +### 3.1 Látogatottság-mérés + +- **Mit:** oldalmegtekintések statisztikája. Az eszköz az **Umami** nevű, **saját üzemeltetésű** + mérőprogram, amely a `stats.felhom.eu` címről töltődik be; adatai nem kerülnek külső + analitikai szolgáltatóhoz. + + +- **Süti:** a mérőprogram a telepítési leírása szerint **nem használ sütit**. + [[ELLENŐRIZNI]] — ez a gyártó állítása, nem mért tény. + +- **Milyen adat keletkezik pontosan** (pl. IP-címből származtatott ország, böngészőtípus, + hivatkozó oldal): [[ELLENŐRIZNI]] — a konfiguráció ezt nem rögzíti. +- **Hol:** a Felhom saját szerverén (k3s fürt, „DooPlex"), ország: [[ELLENŐRIZNI]]. + +- **Meddig:** a mérőprogramban nincs beállított törlési idő. [[ELLENŐRIZNI — megőrzési idő + meghatározandó]] + +- **Jogalap (javaslat, ügyvéd ellenőrzi):** jogos érdek, GDPR 6. cikk (1) f). + +### 3.2 Szervernaplók + +A weboldalt kiszolgáló webszerver (nginx) a kéréseket — köztük a látogató IP-címét — naplózhatja. +A konfiguráció külön naplózási beállítást nem tartalmaz; hogy mi és meddig marad meg: +[[ELLENŐRIZNI]]. + + +### 3.3 Külső tartalmak, sütik + +A weboldal nem tölt be külső betűtípust, külső szkriptet vagy beágyazott tartalmat (a saját +mérőprogramon kívül), és nem helyez el reklám- vagy követő sütit. + + +A `felhom.eu` domain névszerverét a Cloudflare üzemelteti **csak DNS** módban (a forgalom nem halad +át a Cloudflare-en). [[ELLENŐRIZNI — a README ezt állítja; élőben nem mértük]] + + +## 4. Kapcsolatfelvételi űrlap és e-mail + +- **Mit:** név, e-mail-cím, a választott tárgy, az üzenet szövege és a csatolt fájlok (legfeljebb + 5 fájl, összesen 20 MB). A rejtett „website" mező csak a kéretlen üzenetek kiszűrésére szolgál. + +- **Cél:** a megkeresés megválaszolása, illetve a választott tárgy szerint a zárt tesztre + jelentkezés, érdeklődés, árajánlat vagy támogatás. + +- **Útja:** a böngésző a `felhom.eu/api/contact` címre küldi; a Felhom saját kis programja + („contact-mailer") e-mailként továbbítja a **Resend** levélküldő szolgáltatáson keresztül az + `info@felhom.eu` címre; a beérkező leveleket a **Cloudflare Email Routing** továbbítja a + **Gmail**-fiókba, ahol olvassuk. + + + +- **Meddig:** amíg a megkeresés lezárul, majd [[ELLENŐRIZNI — megőrzési idő meghatározandó]]. A + levélfiókban, a Resend-nél és a contact-mailer naplójában maradó másolatok ideje: + [[ELLENŐRIZNI]] — a contact-mailer forráskódja nincs a repóban, így nem ellenőriztük, mit naplóz. + +- **Jogalap (javaslat, ügyvéd ellenőrzi):** az érintett hozzájárulása, GDPR 6. cikk (1) a); árajánlat + és szerződéskötés előtti lépés esetén GDPR 6. cikk (1) b). + +Ha közvetlenül az `info@felhom.eu` címre ír, ugyanez az út érvényes a Resend kivételével. + + +## 5. Előfizetők (háztartások) adatai a Felhom központi rendszerében („hub") + +A központi rendszer (`hub.felhom.eu`) a Felhom saját szerverén fut (k3s fürt), ország: +[[ELLENŐRIZNI]]. + + +| Adat | Mire kell | Meddig marad meg | +|---|---|---| +| Ügyfél azonosítója, neve, domainje, e-mail-címe, nyelve | szerződés teljesítése, értesítések | az ügyfél törléséig | +| A szerver állapotjelentései (gépnév, processzor-, memória-, lemezhasználat, hőmérséklet, a telepített alkalmazások neve és állapota, mentések állapota, a dashboard nyelve) | felügyelet, hibajelzés | **90 nap** | +| Események (pl. „mentés sikertelen", „lemez megtelt") | felügyelet, ügyfélnek látható napló | **90 nap** | +| Alkalmazásonkénti erőforrás-statisztika | kapacitástervezés | **90 nap** | +| Alkalmazásnaplókból kiszűrt hibaüzenetek, előtte-utána 5 sor, kitakarással | hibaelhárítás | az utolsó előfordulás után **30 nap** | +| Alkalmazásnapló-részlet, csak külön kérésre, kitakarással | hibaelhárítás | alkalmazásonként a **legutóbbi 2**, időkorlát nélkül; az ügyfél törlésekor törlődik | +| Diagnosztikai naplócsomag, csak külön kérésre | hibaelhárítás | **72 óra** | +| Kiküldött értesítések naplója (esemény, szöveg, kézbesítés állapota) | elszámolhatóság | **nincs törlési idő; az ügyfél törlése után is megmarad** — lásd 11. pont | +| Az ügyfél-visszaállítás és a szervertörlés naplója | elszámolhatóság | **nincs törlési idő** | +| **A mentés titkosító kulcsa, a háztartás helyreállító kódjával lezárva** („kulcsletét") | a mentés visszaállíthatósága gépcsere után | a szerver / ügyfél törléséig; a lecserélt régi kulcsok is megmaradnak, hogy a régi mentések nyithatók maradjanak | +| A szerver vészhelyzeti konzoljelszava, titkosítva tárolva | üzemeltetés, hibaelhárítás | a szerver törléséig | + +**A kulcsletétről, egyszerűen:** a mentés kulcsát a Felhom csak lezárt formában tárolja. Kinyitni +csak a háztartás **helyreállító kódjával** lehet, amely a Felhomnál nincs meg. Ha a háztartás +elveszíti ezt a kódot, a távoli mentés nem nyitható ki. + + +**A központi rendszer adatbázisáról mentés készül:** éjjelente helyben (a legutóbbi 2 marad meg), +és titkosítva a távoli mentőszerverre („ep0", lásd 6. pont), ahol **14 napi és 8 heti** példány +marad meg; ennek a mentőszervernek a másolata a Felhom saját szerverén **8 heti** példányt tart. +Ezért egy törölt ügyfél adatai a mentésekben a fenti ideig még megtalálhatók. + + + + +**Jogalap (javaslat, ügyvéd ellenőrzi):** szerződés teljesítése, GDPR 6. cikk (1) b); a naplók +megőrzésére jogos érdek, 6. cikk (1) f). + +## 6. A háztartás adatainak távoli mentése (adatfeldolgozás) + +A háztartás szerverén lévő adatokról két távoli mentés készül. **Mindkettő a szerveren titkosítva +készül, mielőtt elhagyja a háztartást**; a tárhely üzemeltetője és a Felhom a titkosított tartalmat +nem tudja elolvasni (a kulcsot lásd az 5. pont „kulcsletét" sorában). + + +| Mentés | Hol | Mit tartalmaz | Meddig | +|---|---|---|---| +| Alkalmazásonkénti fájlmentés | **Hetzner Storage Box**, helyszínkód `fsn1`, ország: [[ELLENŐRIZNI]] | alkalmazásadatok, adatbázisok, megosztások | 7 napi, 4 heti, 6 havi példány; ügyfél-visszaállításkor (RESET) törlődik. A tárhely saját napi pillanatképeinek ideje: [[ELLENŐRIZNI]] | +| Teljes szervermentés | **„ep0"** távoli mentőszerver, Hetzner Cloud, **Nürnberg (Németország)** | a teljes ügyfélkonténer | a legutóbbi 2 heti példány; az ügyfél törlésekor törlődik | +| Az ep0 másolata | a Felhom saját szerverén, ország: [[ELLENŐRIZNI]] | a fenti, továbbra is titkosítva | 8 heti példány — **a törlés után is**, lásd 11. pont | + + + + + +## 7. Hozzáférés a háztartás szerveréhez + +Őszintén: **a Felhom üzemeltetőjének rendszergazdai (root) hozzáférése van minden általa kezelt +szerverhez.** Ez kell a frissítésekhez, a mentések ellenőrzéséhez és a hibaelhárításhoz. Ezzel a +hozzáféréssel a szerveren tárolt adatok elérhetők. A hozzáférést csak [[CÉL ÉS FELTÉTELEK — pl. +hibaelhárítás, az ügyfél kérésére / tudtával]] használjuk. + + +Ami a hozzáférésen túl a központi rendszerbe jut, azt az 5. pont sorolja fel: állapotadatok és — +csak hibaelhárításhoz, külön kérésre — kitakart naplórészletek. A háztartás fájljai és alkalmazásadatai +nem kerülnek a központi rendszerbe. + + +Az üzemeltetői műveletek ügyfél által látható naplója: [[ELLENŐRIZNI — a terv (01 §4) ezt ígéri; +hogy minden művelet valóban megjelenik-e, nem ellenőriztük]]. + + +## 8. A háztartás alkalmazásainak elérése az interneten (Cloudflare) + +A háztartás alkalmazásai és vezérlőpultja a **Cloudflare Tunnel** szolgáltatáson keresztül érhetők +el az internetről. **A titkosított kapcsolat a Cloudflare hálózatán végződik**, vagyis az +alkalmazások forgalma (a látogató IP-címe, a kérések, a továbbított tartalom) a Cloudflare +rendszerén áthalad. A háztartás földrajzi korlátozást is beállíthat, amelyet a központi rendszer a +Cloudflare-en érvényesít. + + + + +Hogy a háztartás domainje kinek a Cloudflare-fiókjában van, és ki a Cloudflare szerződéses +partnere: [[ELLENŐRIZNI — a telepítési leírás szerint a tunnelt az üzemeltető hozza létre a +Cloudflare-felületen; a fiók tulajdonosa nincs rögzítve]]. + + +## 9. Adatfeldolgozók és címzettek + +| Szolgáltató | Mit csinál | Milyen adatot lát | Ország | +|---|---|---|---| +| **Resend** | e-mail-küldés: az űrlap levelei; a központi rendszer levelei a háztartásoknak (értesítés, beállító kód, összekapcsoló link) és az üzemeltetőnek | címzett e-mail-címe, a levél tartalma | [[ELLENŐRIZNI]] — a küldő domain DNS-bejegyzése `eu-west-1` régióra mutat | +| **Cloudflare** | (a) a háztartások alkalmazásainak internetes elérése (Tunnel); (b) a `@felhom.eu` címekre érkező levelek továbbítása (Email Routing); (c) a `felhom.eu` DNS | (a) az alkalmazások teljes forgalma; (b) a beérkező levelek; (c) — | [[ELLENŐRIZNI]] | +| **Google (Gmail)** | a `info@` és `admin@felhom.eu` címre érkező levelek postafiókja | a beérkező levelek, köztük az űrlap üzenetei | [[ELLENŐRIZNI]] | +| **Hetzner** | (a) Storage Box: alkalmazásonkénti titkosított mentés; (b) Cloud szerver „ep0": titkosított teljes szervermentés és a központi adatbázis titkosított mentése | csak titkosított adat; az ügyfél azonosítója a tárhely nevében/címkéjében | (a) `fsn1` helyszínkód, [[ELLENŐRIZNI]]; (b) Németország (Nürnberg) | +| **A Felhom saját szervere** (nem külső szolgáltató) | weboldal, mérőprogram, űrlap-továbbító, központi rendszer, az ep0 másolata | lásd 3–6. pont | [[ELLENŐRIZNI]] | + + + + + + +Az adatokat harmadik országba [[ELLENŐRIZNI — az ügyvéd mondja meg, mely szolgáltatónál van +EGT-n kívüli továbbítás, és milyen garanciával]]. + +## 10. Az érintett jogai + +[[ÜGYVÉD TÖLTI KI — hozzáférés, helyesbítés, törlés, korlátozás, adathordozhatóság, tiltakozás, +hozzájárulás visszavonása; a kérés módja (az 1. pontban megadott adatvédelmi e-mail-címen); válaszidő]] + +Panasz: Nemzeti Adatvédelmi és Információszabadság Hatóság (NAIH) — [[NAIH ELÉRHETŐSÉG]]; +bírósági jogérvényesítés: [[ÜGYVÉD TÖLTI KI]]. + +## 11. Ahol a mai weboldal és a rendszer nem egyezik (javítandó a közzététel előtt) + +1. **„az adataid soha nem hagyják el a hálózatodat"** (GYIK). Nem igaz: a távoli mentések + (titkosítva) Hetzner-tárhelyre kerülnek, az alkalmazások forgalma a Cloudflare-en halad át, + állapotjelentések és kérésre naplórészletek a központi rendszerbe jutnak. + +2. **„az adataid a saját infrastruktúrádon vannak — nem harmadik félnél"** (GYIK, GDPR-kérdés). + A távoli mentés harmadik félnél (Hetzner) van, titkosítva. + +3. **„önálló modell: … a távoli hozzáférést eltávolítjuk … a mentési kulcsokat kizárólag te + ismered"** (GYIK). Hogy ez a termékben létező, megrendelhető mód-e: [[ELLENŐRIZNI]]; a + rendszerleírás szerint a root hozzáférés a termék része. + +4. **Az űrlap hozzájárulási szövege: „Az adatokat harmadik félnek nem adjuk ki."** Az üzenetet a + Resend, a Cloudflare és a Google adatfeldolgozóként kezeli. Javasolt új szöveg: + `DRAFT-kapcsolat-hozzajarulas.md`. + +5. **Törlés után megmaradó adatok.** A kiküldött értesítések naplója nem törlődik soha; a teljes + szervermentés Felhom-oldali másolata a törlés után is megtartja az utolsó 8 heti példányt, és + egyetlen dokumentum sem mondja, mikor törlődnek. Ezt a tájékoztató csak akkor ígérheti + másképp, ha a rendszer változik. + + +## 12. Az ügyvédnek ellenőrizni (R-802) — és ahol a vázlat találgatott + +**Ellenőrizni:** + +1. Minden jogalap (3.1, 4, 5. pont) — a vázlat csak jelöltet nevez meg. +2. Adatkezelő vagy adatfeldolgozó a Felhom a háztartás szerverén lévő adatokra, és mi kerüljön az + adatfeldolgozási megállapodásba (R-809). +3. EGT-n kívüli továbbítás és garanciái: Resend, Cloudflare, Google. +4. Kell-e sütitájékoztató / hozzájárulás a mérőprogramhoz, ha valóban nem használ sütit. +5. A megőrzési idők: hol kell rövidebb (pl. a soha nem törlődő értesítési napló), hol kötelező + hosszabb (számviteli bizonylatok — [[ELLENŐRIZNI]]). +6. A root hozzáférés (7. pont) bemutatása elég-e, és mit kell róla a szerződésnek mondania. +7. A háztartás látogatóinak (pl. családtagok, vendégek) adatai a Cloudflare-en áthaladó forgalomban: + kinek a felelőssége, és kell-e róla tájékoztatni. +8. A zárt teszt résztvevőire (tesztelői megállapodás — R-809) ugyanez a tájékoztató vonatkozik-e. +9. A „háztartási kivétel" állítás a GYIK-ben (gyik.html:204) megállja-e a helyét. + +**Ahol a vázlat találgatott vagy nem talált forrást:** + +1. A Felhom saját szerverének („DooPlex") országa — a dokumentumok nem mondják ki. +2. A Storage Box `fsn1` helyszínkódjának országa — a kód és a dokumentumok csak a kódot írják. +3. A Resend, a Cloudflare és a Google adatkezelési helye — sehol nincs rögzítve; a Resend `eu-west-1` + csak egy DNS-bejegyzésből látszik. +4. Hogy a mérőprogram valóban nem használ sütit, és milyen adatot rögzít — csak a gyártói megjegyzés. +5. A mérőprogram és a webszerver naplóinak megőrzési ideje — nincs beállítva / nincs leírva. +6. A contact-mailer mit naplóz — forrása nincs a repóban. +7. A levelek megőrzése a Gmail-fiókban és a Resend-nél. +8. A Storage Box saját napi pillanatképeinek megőrzési ideje. +9. Kinek a Cloudflare-fiókjában van a háztartás domainje. +10. Hogy az ügyfél által látható üzemeltetői napló minden műveletet tartalmaz-e (tervezési állítás). +11. A README szerint a weboldal DNS-e „csak DNS" módban fut a Cloudflare-en — élőben nem mértük. +12. A hub központi adatbázisának k3s/Longhorn szintű mentései, ha vannak — nem kerestük. diff --git a/documentation/legal/DRAFT-aszf.md b/documentation/legal/DRAFT-aszf.md new file mode 100644 index 00000000..486e3a4d --- /dev/null +++ b/documentation/legal/DRAFT-aszf.md @@ -0,0 +1,170 @@ +# DRAFT — Általános Szerződési Feltételek (Felhom) + +> **English summary.** First draft of the Felhom general terms (ÁSZF), written 2026-10-08 for +> R-813/R-809. NOT published, NOT legal advice, pending lawyer review (R-802). It is a structured +> skeleton: the service description is drawn from the architecture documents (each cited in an HTML +> comment); every business term — prices, term, notice periods, liability caps, the company's +> identity — is a placeholder (`[[...]]`). Nothing commercial is invented. Section 14 lists what the +> lawyer must check and every point where the draft guessed. + +--- + +**Hatály:** [[HATÁLYBALÉPÉS DÁTUMA]] · **Verzió:** [[VERZIÓ]] + +## 1. A szolgáltató + +| | | +|---|---| +| Név | [[CÉGNÉV]] | +| Székhely | [[SZÉKHELY]] | +| Cégjegyzékszám / nyilvántartási szám | [[CÉGJEGYZÉKSZÁM]] | +| Adószám | [[ADÓSZÁM]] | +| E-mail | [[KAPCSOLATI E-MAIL]] | +| Telefon | [[TELEFON]] | +| Panaszkezelés helye és módja | [[PANASZKEZELÉS]] | + +## 2. Fogalmak + +- **Felhom / Szolgáltató:** az 1. pontban megnevezett vállalkozás. +- **Előfizető:** a szolgáltatást megrendelő [[természetes személy / háztartás — ELLENŐRIZNI: + fogyasztó-e]]. +- **Szerver:** az Előfizető otthonában működő számítógép, amelyen a Felhom rendszere fut. +- **Alkalmazás:** a Felhom katalógusából a Szerverre telepíthető program (pl. fénykép-, dokumentum-, + jelszókezelő). +- **Vezérlőpult:** a Szerver webes kezelőfelülete (`felhom.`). +- **Helyreállító kód:** az Előfizetőnél lévő kód, amely nélkül a távoli mentés nem nyitható ki. + +## 3. A szolgáltatás tárgya + +A Felhom egy **otthoni szervert** üzemeltet az Előfizető háztartásában, és ehhez a következőket +nyújtja: + + +1. **A szerver telepítése és beállítása** — személyes jelenléttel vagy előre telepítve átadva. + +2. **Alkalmazások** telepítése a Felhom katalógusából, a vezérlőpulton keresztül. + +3. **Internetes elérés** a háztartás saját domainjén, a Cloudflare Tunnel szolgáltatáson keresztül. + A domain díja [[A DÍJ RÉSZE / KÜLÖN FIZETENDŐ — a tervezési döntés szerint a díj része]]. + +4. **Felügyelet:** a szerver állapotjelentést küld a Felhom központi rendszerének; hiba esetén a + Felhom és — beállítástól függően — az Előfizető e-mailt kap. + +5. **Mentések** több szinten: + - helyi mentés a szerveren és (ha van) második meghajtón; + - **távoli mentés**, titkosítva, a Felhom által bérelt tárhelyen (Hetzner); + - **teljes szervermentés** a Felhom távoli mentőszerverére. + A mentések részletei és megőrzési ideje: [[MELLÉKLET / ADATKEZELÉSI TÁJÉKOZTATÓ 6. PONT]]. + +6. **Frissítések:** az alkalmazások, a rendszer és az operációs rendszer frissítése, jellemzően + éjszakai időablakban, amelyet az Előfizető állíthat be. + +7. **Ügyfélszolgálat:** [[ELÉRHETŐSÉG, VÁLASZIDŐ, NYITVATARTÁS]]. + +A szolgáltatás **nem** tartalmazza: [[KIZÁRÁSOK — pl. az Előfizető saját eszközeinek javítása, +internet-előfizetés, áramellátás]]. + +## 4. Hardver + +[[A SZERVER TULAJDONJOGA: a Felhom adja (bérlet / eladás) VAGY az Előfizető saját gépe — a rendszer +mindkét telepítési módot ismeri („appliance" és „byo")]]. Garancia, csere, visszaszolgáltatás a +szerződés végén: [[FELTÉTELEK]]. + + +## 5. Az Előfizető kötelezettségei + +1. Biztosítja a szerver áramellátását és internetkapcsolatát [[RÉSZLETEK]]. +2. **Megőrzi a helyreállító kódot.** A kódot a Felhom nem ismeri és nem tudja pótolni; elvesztése + esetén a távoli mentés nem állítható vissza. + +3. A vezérlőpult jelszavát titokban tartja. +4. Az alkalmazásokat jogszerűen használja; a harmadik féltől származó alkalmazások saját licencfeltételei + rá is vonatkoznak. + +5. [[TOVÁBBI KÖTELEZETTSÉGEK]] + +## 6. A Felhom hozzáférése a szerverhez + +A Felhom üzemeltetőjének **rendszergazdai (root) hozzáférése van** a szerverhez. Ezt +frissítésre, mentésellenőrzésre és hibaelhárításra használja. A szerver a Felhom felé maga +kezdeményez kapcsolatot; a veszélyes (adatvesztéssel járó) műveletekhez az üzemeltető külön +aláírása kell. + + + +A hozzáférés használatának feltételei, az Előfizető értesítése: [[FELTÉTELEK]]. +Az „önálló modell" (a távoli hozzáférés eltávolítása) elérhetősége: [[ELLENŐRIZNI — a GYIK +ígéri (website/gyik.html:669); a rendszerleírásban nem találtuk]]. + +## 7. Az adatok az Előfizetőéi + +Az Előfizető adatai és telepített alkalmazásai az Előfizetőéi. A Felhom nem tartja vissza őket: a +szerződés megszűnése után is visszaállíthatók az Előfizető helyreállító kódjával. + + +A szerződés megszűnésekor: [[ADATÁTADÁS, A TÁVOLI MENTÉSEK TÖRLÉSÉNEK IDEJE — lásd az +adatkezelési tájékoztató 11. pont 5.: a Felhom-oldali másolat ma nem törlődik határidőre]]. + + +## 8. Díjak és fizetés + +| Tétel | Díj | +|---|---| +| Telepítés | [[DÍJ]] | +| Havi / éves előfizetés | [[DÍJ]] | +| Hardver | [[DÍJ / BÉRLETI DÍJ / NEM ÉRTELMEZETT]] | +| Domain | [[DÍJ — vagy: az előfizetés része]] | +| Távoli mentés tárhely-bővítése | [[DÍJ]] | + +Fizetési mód, számlázás, késedelem: [[FELTÉTELEK]]. Áremelés: [[FELTÉTELEK]]. + + +## 9. A szolgáltatás szintje + +Rendelkezésre állás: [[VÁLLALT SZINT — vagy: nincs vállalt szint]]. A Felhom a szerver +elérhetetlenségét figyeli és jelzi; az otthoni áram- és internetkimaradás nem a Felhom hibája. +Hibabejelentés és válaszidő: [[FELTÉTELEK]]. + +## 10. Felelősség + +[[ÜGYVÉD TÖLTI KI — felelősségkorlátozás, vis maior, adatvesztés: a mentések megléte nem +garantálja minden adat visszaállíthatóságát; az elveszett helyreállító kód következménye]]. + +## 11. A szerződés időtartama és megszűnése + +Időtartam: [[HATÁROZOTT / HATÁROZATLAN]]. Felmondási idő: [[NAP]]. Elállási jog (fogyasztó +esetén): [[ÜGYVÉD TÖLTI KI]]. A Felhom általi felmondás esetei: [[FELTÉTELEK]]. + +## 12. Adatkezelés + +Az adatkezelésről az **Adatkezelési tájékoztató** [[LINK]] szól; a szerveren tárolt adatok +feldolgozásáról az **Adatfeldolgozási megállapodás** [[LINK — még nincs megírva, R-809]]. + +## 13. Vegyes rendelkezések + +Irányadó jog: [[ÜGYVÉD TÖLTI KI]]. Vitarendezés, békéltető testület: [[ÜGYVÉD TÖLTI KI]]. +Az ÁSZF módosítása és annak közlése: [[FELTÉTELEK]]. + +## 14. Az ügyvédnek ellenőrizni (R-802) — és ahol a vázlat találgatott + +**Ellenőrizni:** + +1. Fogyasztói szerződés-e (háztartás) — elállási jog, tájékoztatási kötelezettségek, távollévők + közötti szerződés szabályai. +2. A harmadik féltől származó alkalmazások licencei (R-802 listája: Tandoor, SparkyFitness, Emby, + n8n, Plex, EE/BUSL részek, redis 7.4) — mit kell az ÁSZF-nek mondania róluk. +3. A root hozzáférés (6. pont) leírása elég-e, és milyen feltételhez kell kötni. +4. Felelősségkorlátozás adatvesztésre, különösen ha az Előfizető elveszíti a helyreállító kódot. +5. A szerződés megszűnésekor az adatok sorsa — a rendszerben ez még nincs megtervezve. +6. A zárt teszt résztvevőire (tesztelői megállapodás, R-809 / R-11) milyen feltételek vonatkoznak. + +**Ahol a vázlat találgatott:** + +1. Hogy az Előfizető fogyasztó — a vázlat háztartást feltételez a projekt leírása alapján. +2. A két telepítési mód („appliance" / „byo") üzleti jelentése (bérelt vagy saját gép) — sehol + nincs leírva. +3. Az „önálló modell" létezése — csak a GYIK állítja. +4. A 3. pont szolgáltatáslistája a műszaki dokumentumokból készült; hogy ebből mi a fizetős + csomag része, üzleti döntés. +5. Hogy a domain díja az előfizetés része — tervezési döntés (01 §7), nem árlista. +6. Az ügyfél által látható üzemeltetői napló — tervezési állítás, megvalósítását nem ellenőriztük. diff --git a/documentation/legal/DRAFT-impresszum.md b/documentation/legal/DRAFT-impresszum.md new file mode 100644 index 00000000..0d25b3e4 --- /dev/null +++ b/documentation/legal/DRAFT-impresszum.md @@ -0,0 +1,49 @@ +# DRAFT — Impresszum (felhom.eu) + +> **English summary.** First draft of the felhom.eu imprint, written 2026-10-08 for R-813. NOT +> published, NOT legal advice, pending lawyer review (R-802). Every fact about the operator is a +> placeholder; the only facts filled in are the hosting arrangements, each cited. The list at the end +> says what the lawyer must check and where the draft guessed. + +--- + +## A weboldal üzemeltetője + +| | | +|---|---| +| Név | [[CÉGNÉV]] | +| Cégforma | [[CÉGFORMA — pl. egyéni vállalkozó / Kft.]] | +| Székhely | [[SZÉKHELY]] | +| Postacím | [[POSTACÍM]] | +| Cégjegyzékszám / egyéni vállalkozói nyilvántartási szám | [[CÉGJEGYZÉKSZÁM]] | +| Nyilvántartó hatóság / cégbíróság | [[NYILVÁNTARTÓ]] | +| Adószám | [[ADÓSZÁM]] | +| Közösségi adószám | [[KÖZÖSSÉGI ADÓSZÁM — ha van]] | +| Képviselő | [[KÉPVISELŐ NEVE]] | +| E-mail | [[KAPCSOLATI E-MAIL]] | +| Telefon | [[TELEFON]] | +| Kamarai tagság | [[KAMARA — ha van]] | + +## Tárhely + +A weboldalt a Felhom **saját szervere** szolgálja ki; külső tárhelyszolgáltató nincs. A szerver +helye: [[ORSZÁG / CÍM — ELLENŐRIZNI]]. + + +A `felhom.eu` domain névszerverét a **Cloudflare** biztosítja (csak DNS). +[[CLOUDFLARE CÉGADATAI — ELLENŐRIZNI, ha az ügyvéd szerint fel kell tüntetni]] + + +## Kapcsolódó dokumentumok + +- Általános Szerződési Feltételek — [[LINK]] +- Adatkezelési tájékoztató — [[LINK]] + +## Az ügyvédnek ellenőrizni (R-802) — és ahol a vázlat találgatott + +1. Mely adatok kötelezők egy szolgáltatást kínáló weboldal impresszumában (Ekertv. szerinti + tájékoztatás) — a táblázat a szokásos mezőket sorolja, nem jogi listát. +2. Kell-e saját szerver esetén tárhelyszolgáltatót megnevezni, és kell-e a Cloudflare-t (csak DNS). +3. **Találgatás:** hogy nincs külső tárhelyszolgáltató — a README és a manifestek szerint a webszerver + a Felhom saját k3s fürtjén fut; élőben nem mértük. +4. **Találgatás:** a cégforma — a dokumentumok semmit nem mondanak a vállalkozásról. diff --git a/documentation/legal/DRAFT-kapcsolat-hozzajarulas.md b/documentation/legal/DRAFT-kapcsolat-hozzajarulas.md new file mode 100644 index 00000000..15ac54e1 --- /dev/null +++ b/documentation/legal/DRAFT-kapcsolat-hozzajarulas.md @@ -0,0 +1,51 @@ +# DRAFT — A kapcsolatfelvételi űrlap hozzájárulási szövege + +> **English summary.** A proposed replacement for the contact form's consent checkbox text +> (`website/kapcsolat.html:122-128`), written 2026-10-08 for R-813. NOT applied to the website, +> NOT legal advice, pending lawyer review (R-802). The current text names no controller, no +> retention, no rights, links nowhere, and says the data is not given to third parties although +> three processors (Resend, Cloudflare Email Routing, Gmail) carry the message. The proposal links to +> the privacy notice, which must be published first. + +## A mai szöveg + + + +> Elfogadom, hogy az űrlapon megadott adataimat a Felhom.eu a megkeresésem megválaszolásához +> felhasználja. Az adatokat harmadik félnek nem adjuk ki. + +**Mi a gond vele:** + +1. Nem nevezi meg az adatkezelőt (a „Felhom.eu" egy domain, nem jogi személy). +2. Nem mondja meg, meddig őrizzük az adatokat, és milyen jogai vannak a kitöltőnek. +3. Nem hivatkozik adatkezelési tájékoztatóra — ilyen ma nincs is (R-813). +4. **„Harmadik félnek nem adjuk ki"** — az üzenetet a Resend (levélküldés), a Cloudflare Email + Routing (továbbítás) és a Google Gmail (postafiók) adatfeldolgozóként kezeli. + +5. A hibaüzenet „adatkezelési hozzájárulás"-t említ, de a jelölőnégyzet egyben az üzenet + elküldésének feltétele — hogy a hozzájárulás itt a helyes jogalap-e, az ügyvéd dönti el. + + +## Javasolt szöveg + +> Elolvastam az [Adatkezelési tájékoztatót](/adatkezeles), és hozzájárulok, hogy a(z) +> [[CÉGNÉV]] a megadott nevemet, e-mail-címemet, üzenetemet és csatolt fájljaimat a megkeresésem +> megválaszolására kezelje. Az üzenetet levélküldő és levelezési szolgáltatók (adatfeldolgozók) +> továbbítják; a részleteket, a megőrzés idejét és a jogaimat a tájékoztató írja le. A +> hozzájárulásomat bármikor visszavonhatom a(z) [[ADATVÉDELMI E-MAIL]] címen. + +**A link:** `/adatkezeles` — [[ELLENŐRIZNI: a végleges útvonal]]. A weboldal oldalai ma +kiterjesztés nélküli útvonalon érhetők el (pl. `kapcsolat.html` → `/kapcsolat`), ezért a javasolt +fájlnév `website/adatkezeles.html`. A linket érdemes új lapon nyitni (`target="_blank" rel="noopener"`), +hogy a kitöltött űrlap ne vesszen el. + + +**A hibaüzenet** maradhat: „Az adatkezelési hozzájárulás szükséges." — vagy, ha az ügyvéd szerint a +jogalap nem hozzájárulás, akkor: „Kérlek, jelezd, hogy elolvastad az adatkezelési tájékoztatót." + +## Közzététel előtt (nem most) + +1. Az ügyvéd jóváhagyja a szöveget és a jogalapot. +2. Az adatkezelési tájékoztató megjelenik a megadott útvonalon. +3. A szöveg kicserélése a `website/kapcsolat.html` 122–128. sorában, és a lábléc linkjei minden oldalon. +4. A placeholderek kitöltése. diff --git a/documentation/legal/README.md b/documentation/legal/README.md new file mode 100644 index 00000000..ea3028b8 --- /dev/null +++ b/documentation/legal/README.md @@ -0,0 +1,32 @@ +# Legal drafts — NOT published, NOT legal advice + +**Status: first drafts, written 2026-10-08 by Claude Code on the operator's request (Part E of that +night's brief). Pending lawyer review. Nothing here is on the website.** + +These files are working drafts for register rows **R-813** (the website collects personal data but +publishes no privacy notice, no terms and no imprint — owner: operator) and **R-802** (the lawyer's +review before the first paying customer — owner: operator). The wider set of business papers +(customer contract, data-processing agreement, billing) is the intention **R-809** in +`documentation/backlog/ROADMAP.md`. + +| File | What it is | +|---|---| +| `DRAFT-aszf.md` | Általános Szerződési Feltételek — a structured skeleton; business terms are placeholders | +| `DRAFT-adatkezelesi-tajekoztato.md` | Adatkezelési tájékoztató — built from the architecture documents and the code, each line cited | +| `DRAFT-impresszum.md` | Impresszum — placeholders only | +| `DRAFT-kapcsolat-hozzajarulas.md` | A proposed replacement for the contact form's consent text, and the link it should carry | + +## Rules for these drafts + +- **Not legal advice.** Written by an AI assistant from the system's own documents. A lawyer decides + what is legally required, what legal basis applies, and what the final text says. +- **Every fact the operator must supply is a visible placeholder** like `[[CÉGNÉV]]`. Nothing is + invented. `[[ELLENŐRIZNI]]` marks a fact the code and the documents do not state. +- **The privacy notice is true to the system, not to a template.** Each line carries an HTML comment + ``. If the system changes (a new processor, a new retention), the notice + must change with it. +- **Nothing is published until the operator says so.** Publishing means a website change (`website/` + is served from `main` by git-sync), a link from every page footer, and the contact form's consent + text replaced — none of which is done here. +- Each draft ends with the list of points the lawyer must check and every point where the draft had + to guess.