2026-10-08 day: legal drafts (R-813), R-304 design, R-232(a) evidence, R-762 bench proof; R-899/R-243/R-304/R-232/R-762 updated; R-900, R-901 opened; 127 -> 130 (R-902 by the website session)
gates / gates (push) Successful in 3m43s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 08:36:03 +02:00
parent 663421ddfb
commit c7aade4079
43 changed files with 3412 additions and 9 deletions
+42
View File
@@ -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.
+17 -2
View File
@@ -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
@@ -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/<id>/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.
@@ -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).
@@ -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 <monitoring@felhom.eu>"
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
}
@@ -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
}
@@ -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
@@ -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.
@@ -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
}
@@ -0,0 +1 @@
174047232
@@ -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=<image-default, redacted> 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/<image-default, redacted>/<image-default, redacted>/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
@@ -0,0 +1 @@
cgroup: /sys/fs/cgroup/system.slice/docker-1dd3339a4eeb9ed63540b35d7ca09acca030259a68038e233e7270947dadcb05.scope
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
WGER_USE_GUNICORN=True
WEB_CONCURRENCY=2
@@ -0,0 +1,2 @@
oom 0
oom_kill 0
@@ -0,0 +1 @@
restarts=0 oomkilled=false status=running health=starting
@@ -0,0 +1 @@
402653184
@@ -0,0 +1 @@
402653184
@@ -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
@@ -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
@@ -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 '<image-default, redacted>'
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
@@ -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 <wger@example.com>}
- 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
@@ -0,0 +1,14 @@
{
"wger": {
"status": "running",
"health": "healthy",
"restarts": 0,
"exit": 0
},
"wger-files": {
"status": "running",
"health": "healthy",
"restarts": 0,
"exit": 0
}
}
@@ -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
@@ -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.
@@ -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
@@ -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.
@@ -0,0 +1,14 @@
{
"wger": {
"status": "running",
"health": "healthy",
"restarts": 0,
"exit": 0
},
"wger-files": {
"status": "running",
"health": "healthy",
"restarts": 0,
"exit": 0
}
}
@@ -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
}
File diff suppressed because one or more lines are too long
@@ -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.
<!-- source: documentation/backlog/ROADMAP.md:53 (R-809: "a data-processing agreement (Felhom monitors boxes and holds encrypted off-site backups, so it processes household data)") -->
## 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.
<!-- source: website/index.html:56, website/kapcsolat.html:28 (script src https://stats.felhom.eu/script.js) -->
<!-- source: manifests/umami.yaml:1-7 (Umami v3, dedicated PostgreSQL in felhom-system namespace), :190 (image ghcr.io/umami-software/umami:3.1.0) -->
- **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.
<!-- source: manifests/umami.yaml:277 ("cookie-free, and GDPR compliant" — vendor claim in a manifest comment, not measured) -->
- **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]].
<!-- source: README.md:65-73 (k3s single node, felhom-system namespace: umami + umami-db) -->
- **Meddig:** a mérőprogramban nincs beállított törlési idő. [[ELLENŐRIZNI — megőrzési idő
meghatározandó]]
<!-- source: manifests/umami.yaml:194-206 (env: only DATABASE_URL, APP_SECRET, DISABLE_TELEMETRY, TZ — no retention setting) -->
- **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]].
<!-- source: manifests/webpage.yaml (nginx:alpine at :250; grep for access_log/log_format returns nothing — default logging, retention not stated anywhere) -->
### 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.
<!-- source: grep over website/*.html for http(s) hosts returns only github.com, gitlab.com, felhom.eu, stats.felhom.eu, iso.felhom.eu, schema.org, formspree.io (the last in a code comment, kapcsolat.html ~:185); grep for googleapis|gstatic|cdn.|cloudflareinsights in website/ returns nothing -->
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]]
<!-- source: README.md:57 ("Cloudflare (DNS only, no proxy)"), README.md:108 -->
## 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.
<!-- source: website/kapcsolat.html:72-75 (honeypot), :78, :83, :91, :104, :111 (fields); :193-196 (maxTotalFileSize 20 MB, maxFiles 5); :467-476 (fields actually sent) -->
- **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.
<!-- source: website/kapcsolat.html:92-97 (subject options) -->
- **Ú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.
<!-- source: website/kapcsolat.html:191 (formEndpoint '/api/contact'), :477 (fetch POST) -->
<!-- source: manifests/contact-mailer.yaml:1-2 ("Uses Resend.com API"), :66-69 (FROM noreply@felhom.eu, TO info@felhom.eu) -->
<!-- source: README.md:133-135 (Cloudflare Email Routing: info@ and admin@ → personal Gmail), README.md:146-151 (contact form flow) -->
- **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.
<!-- source: find over felhom.eu for a contact-mailer source dir returns nothing; README.md:71 names it a Go app; manifests/contact-mailer.yaml:6 builds it from an unnamed directory -->
- **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.
<!-- source: website/kapcsolat.html:142 (mailto:info@felhom.eu); README.md:133-135 -->
## 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]].
<!-- source: manifests/hub.yaml:111-131 (Deployment felhom-hub, namespace felhom-system), :343 (host hub.felhom.eu); documentation/architecture/01-topology-and-trust.md:82 ("Runs on dooplex.hu (k3s)") -->
| 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 <!-- source: hub/internal/store/store.go:161-171 (customer_configs: customer_id, customer_name, domain, email); :223 (language); store.go DELETE FROM customer_configs; documentation/architecture/00-capability-map.md:97 (Customer DELETE cascade) --> |
| 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** <!-- source: felhom-controller/controller/internal/report/types.go:13-56, :66-79 (report fields); hub/internal/store/store.go:1528-1540 (Prune: reports + host_reports); manifests/hub.yaml:78 (retention.max_days: 90); hub/cmd/hub/main.go:930 (default 90) --> |
| Események (pl. „mentés sikertelen", „lemez megtelt") | felügyelet, ügyfélnek látható napló | **90 nap** <!-- source: hub/internal/store/store.go:2695-2702 (PruneEvents); manifests/hub.yaml:78 --> |
| Alkalmazásonkénti erőforrás-statisztika | kapacitástervezés | **90 nap** <!-- source: hub/cmd/hub/main.go:1022 (PruneAppTelemetry 90 days) --> |
| 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** <!-- source: hub/internal/store/telemetry.go:35 (±5 redacted lines); hub/cmd/hub/main.go:1027 (PruneStaleIssues 30 days) --> |
| 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 <!-- source: felhom-controller/controller/internal/report/types.go:39-41 ("on-demand… Redacted + capped"); hub/internal/store/logtail.go:74-82 (keep newest 2 per customer+app); hub/internal/store/customer_delete.go:57-58 (purged at delete) --> |
| Diagnosztikai naplócsomag, csak külön kérésre | hibaelhárítás | **72 óra** <!-- source: hub/internal/store/logbundle.go:28-29 (logBundleTTL = 72h), :234 --> |
| 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 <!-- source: hub/internal/store/store.go:147-156 (notification_log columns); no DELETE FROM notification_log anywhere in hub/internal/store (grep); hub/internal/store/customer_delete.go:23-25 ("deliberately SURVIVES … notification_log") --> |
| Az ügyfél-visszaállítás és a szervertörlés naplója | elszámolhatóság | **nincs törlési idő** <!-- source: hub/internal/store/store.go:734 ("NEVER pruned (audit outlives every lifecycle tier)"); customer_delete.go:25 --> |
| **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 <!-- source: hub/internal/store/store.go:400-411 (host_escrow: "OPAQUE … NEVER decrypts"), :413-446 (host_escrow_superseded; "NO pruning" at :418); store.go:4122 (PurgeSupersededEscrowForCustomer); documentation/architecture/00-capability-map.md:97 --> |
| A szerver vészhelyzeti konzoljelszava, titkosítva tárolva | üzemeltetés, hibaelhárítás | a szerver törléséig <!-- source: hub/internal/store/store.go:552-564 (host_recovery, CREATE at :558); documentation/architecture/05-hub-architecture.md:441-443 (sealed AES-256-GCM) --> |
**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.
<!-- source: documentation/architecture/07-backup-architecture.md:67-75 ("The escrow is genuinely zero-knowledge today… R exists in zero system copies by design"); documentation/architecture/05-hub-architecture.md:213-214, :233-236 -->
**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.
<!-- source: documentation/architecture/05-hub-architecture.md:466-474 (nightly VACUUM INTO, newest 2 stay; encrypted push to ep0 operator namespace) -->
<!-- source: documentation/runbooks/RUNBOOK-hub-db-offsite-backup.md:72-73 (prune-operator-hubdb --keep-daily 14 --keep-weekly 8) -->
<!-- source: documentation/runbooks/ep0-datastore-copy.md:15-17 (DooPlex pull, remove-vanished false; prune keep-weekly 8, all namespaces) -->
**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).
<!-- source: documentation/architecture/01-topology-and-trust.md:118 (box ↔ PBS: "ciphertext only (operator can't read)"); documentation/architecture/07-backup-architecture.md:378-379 (Encrypted: yes (restic) / yes (per-customer key)) -->
| 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 |
<!-- source (row 1): documentation/architecture/07-backup-architecture.md:377 (Tier-3: Hetzner Storage Box over SFTP; --keep-daily 7 --keep-weekly 4 --keep-monthly 6); manifests/hub.yaml:224-226 (HETZNER_LOCATION "fsn1"); hub/cmd/hub/main.go:386 (default fsn1); documentation/audits/VALIDATION-offsite-provisioning-e2e-2026-07-09.md:6 (pool box BX11 fsn1); documentation/architecture/07-backup-architecture.md:258-266 (RESET purges the repository); hub/internal/monitor/offsite.go:300, :317 (daily Storage Box snapshots read-only) -->
<!-- source (row 2): documentation/runbooks/RUNBOOK-ep0-datastore-volume-2026-07-27.md:6 ("Hetzner CX33 … Nuremberg"); documentation/architecture/07-backup-architecture.md:379 (keep-last 2, server-side prune on ep0); documentation/architecture/05-hub-architecture.md:296 (RESET / customer delete: deprovisioned — namespace, every backup group AND token) -->
<!-- source (row 3): documentation/architecture/06-offsite-connectivity.md:189 (nightly pull-sync ep0 → DooPlex, ciphertext per customer); documentation/runbooks/ep0-datastore-copy.md:15-17 ("It never removes what ep0 removed"; keep-weekly 8) -->
## 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.
<!-- source: documentation/architecture/07-backup-architecture.md:49-52 ("The operator holds root SSH on every box… 'The operator cannot read customer data' was never the security property"), :56-63 (OOB SSH key on every box; break-glass password in the hub; guest data reachable from the host) -->
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.
<!-- source: felhom-controller/controller/internal/report/types.go:13-56 (report contents); documentation/architecture/05-hub-architecture.md:206 ("the actual app data + configs live inside the PBS guest snapshot") -->
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]].
<!-- source: documentation/architecture/01-topology-and-trust.md:105 ("All operator-initiated actions are recorded in a customer-visible audit log") — a [DESIGN]-era statement, not marked [FACT] -->
## 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.
<!-- source: documentation/architecture/01-topology-and-trust.md:113-114 (end-user ↔ apps: Cloudflare Tunnel → Traefik), :120 (hub ↔ Cloudflare API, geo WAF), :202-224 (tunnel; cloudflared inside the guest) -->
<!-- source: documentation/architecture/05-hub-architecture.md:178-180 ("in the Cloudflare-Tunnel-default model the edge terminates TLS") -->
<!-- source: documentation/architecture/01-topology-and-trust.md:131-136 (Cloudflare appends the visitor address, CF-Connecting-IP) -->
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]].
<!-- source: documentation/runbooks/day0-install.md:39-58 (A.1: operator creates the tunnel in the Cloudflare dashboard; "Make sure the domain is on Cloudflare"); documentation/architecture/01-topology-and-trust.md:207-215 (customer's own domain, included in the price) -->
## 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]] |
<!-- source (Resend): hub/internal/notify/dispatcher.go:907 (https://api.resend.com); manifests/contact-mailer.yaml:1-2; documentation/architecture/05-hub-architecture.md:308-318 (the four customer mails; operator channel); manifests/hub.yaml:94 (operator_email); README.md:121 (MX send → feedback-smtp.eu-west-1.amazonses.com) -->
<!-- source (Cloudflare): documentation/architecture/01-topology-and-trust.md:113-114, :120; hub/internal/cloudflare/unblock.go:15 (api.cloudflare.com); README.md:57, :120, :133-136 -->
<!-- source (Google): README.md:134-135 ("forwarded to personal Gmail") -->
<!-- source (Hetzner): hub/internal/hetznerapi/hetznerapi.go:3 (api.hetzner.com); hub/internal/offsite/offsite.go:404-405, :443-448 (labels/description/name carry the customer id); documentation/runbooks/RUNBOOK-ep0-datastore-volume-2026-07-27.md:6; documentation/runbooks/RUNBOOK-hub-db-offsite-backup.md:72-73 -->
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.
<!-- source: website/gyik.html:670; contradicted by §6, §8, §5 above -->
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.
<!-- source: website/gyik.html:204, :691 -->
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.
<!-- source: website/gyik.html:188, :669; documentation/architecture/07-backup-architecture.md:49-52 -->
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`.
<!-- source: website/kapcsolat.html:122-128 -->
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.
<!-- source: hub/internal/store/customer_delete.go:23-25; documentation/runbooks/ep0-datastore-copy.md:15-17 -->
## 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.
+170
View File
@@ -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.<az Előfizető domainje>`).
- **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:
<!-- source: CLAUDE.md (workspace root) "Felhom, a managed home-server service for Hungarian households" -->
1. **A szerver telepítése és beállítása** — személyes jelenléttel vagy előre telepítve átadva.
<!-- source: documentation/architecture/01-topology-and-trust.md:182-183 ("Physical presence at provisioning (on-site install, or pre-imaged-and-delivered)") -->
2. **Alkalmazások** telepítése a Felhom katalógusából, a vezérlőpulton keresztül.
<!-- source: documentation/architecture/01-topology-and-trust.md:85 (controller: Docker/app lifecycle, catalog deploy, customer UI) -->
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]].
<!-- source: documentation/architecture/01-topology-and-trust.md:202-215 (Cloudflare Tunnel; every customer has their own domain, "included in the customer's price") -->
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.
<!-- source: documentation/architecture/05-hub-architecture.md:71-103 (report ingest, liveness, backup-deadline checker), :308-318 (customer mails) -->
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]].
<!-- source: documentation/architecture/07-backup-architecture.md:373-379 (the four tiers as configured on the live fleet) -->
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.
<!-- source: documentation/architecture/07-backup-architecture.md:420 ("The three nightly legs derive from one customer-settable window start W") -->
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]].
<!-- source: scripts/felhom-host-install.sh:39, :79 (--mode appliance|byo); hub/internal/store/store.go:764 (appliance_registrations.install_mode) — the commercial meaning of the two modes is not documented -->
## 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.
<!-- source: documentation/architecture/07-backup-architecture.md:67-75 (zero-knowledge escrow; "R exists in zero system copies by design"); :77-83 (the household is asked for it from the first login) -->
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.
<!-- source: documentation/backlog/OPEN-ITEMS.md:268 (R-802: non-OSI licence list — Tandoor, SparkyFitness, Emby, n8n, Plex, EE/BUSL parts, redis 7.4) -->
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.
<!-- source: documentation/architecture/07-backup-architecture.md:49-52 (operator holds root SSH on every box) -->
<!-- source: documentation/architecture/01-topology-and-trust.md:97-105 (box-initiated control, signed jobs, customer-visible audit log) -->
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.
<!-- source: documentation/architecture/01-topology-and-trust.md:300-302 ("Never hold data hostage … the customer's data and deployed apps remain recoverable by the customer (recovery code), with nothing locked behind the operator") -->
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]].
<!-- source: documentation/architecture/01-topology-and-trust.md:309-310 ("Offboarding / decommission … not yet designed") -->
## 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]].
<!-- source: documentation/backlog/ROADMAP.md:53 (R-809: billing and invoicing — not built) -->
## 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.
+49
View File
@@ -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]].
<!-- source: README.md:65-73 (k3s single node; felhom-system: felhom-webpage nginx + git-sync); manifests/webpage.yaml:433 (host felhom.eu) -->
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]]
<!-- source: README.md:57, :108 ("Cloudflare DNS (free plan), DNS only mode") -->
## 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.
@@ -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
<!-- source: website/kapcsolat.html:122-128 -->
> 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.
<!-- source: manifests/contact-mailer.yaml:1-2, :66-69; README.md:133-135, :146-151 -->
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.
<!-- source: website/kapcsolat.html:123 (required), :128 (privacyError text) -->
## 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.
<!-- source: README.md:39 (kapcsolat.html served at /kapcsolat) -->
**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.
+32
View File
@@ -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
`<!-- source: path:line -->`. 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.