From 195129cbb1474c163932b190cb355c0ff0022746 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 1 Oct 2026 16:58:56 +0200 Subject: [PATCH] =?UTF-8?q?Radicale:=20the=20first=20new=20app=20through?= =?UTF-8?q?=20the=20checklist=20=E2=80=94=20template,=20record,=20fixture,?= =?UTF-8?q?=20first=20ladder=20step=203.8.0=20->=203.8.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calendar and contacts (CalDAV/CardDAV), ghcr.io/kozea/radicale:3.8.1. Login file written from the generated password on the first start only (R-765: rewriting it at every start lost the household's login on a restore). onboarding/radicale.md complete; bench + 9202 proven; FIRST-ADMIN, README, Hungarian freeze. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CHANGELOG.md | 13 +++ FIRST-ADMIN.md | 1 + README.md | 2 + onboarding/radicale.md | 73 ++++++++++++++++ scripts/copy_freeze/hu.json | 25 +++++- scripts/upgrade_fixtures_box.py | 56 +++++++++++++ templates/radicale/.felhom.yml | 116 ++++++++++++++++++++++++++ templates/radicale/docker-compose.yml | 64 ++++++++++++++ 8 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 onboarding/radicale.md create mode 100644 templates/radicale/.felhom.yml create mode 100644 templates/radicale/docker-compose.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1649d3c..d50d0ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## Radicale — the first app through the new-app checklist (2026-10-01, evening) + +- **`templates/radicale/`** — calendar and contacts (CalDAV/CardDAV), the Radicale project's own image + `ghcr.io/kozea/radicale:3.8.1`. The image ships no config and Radicale 3 lets nobody in by default (`denyall`), so the + start command writes a bcrypt login file from the box's generated password on the FIRST start only, then runs + Radicale with htpasswd auth. First admin class 1; no setup gate (401 from the first answer). Its first ladder step + 3.8.0 -> 3.8.1 proven on the bench (swap 0) and on 9202, written by `upgrade-test.py --write-ladder`. +- **Found and fixed before publishing (R-765):** the first template rewrote the login file at EVERY start; a remove + + restore then left the household with a password no page shows (a restore regenerates `type: password` values by + design, expecting the app's own login to come back with its data). Written once, it does — measured. +- `onboarding/radicale.md` (every id done or n/a), fixture `Radicale` in `upgrade_fixtures_box.py` (CalDAV seed, + three negative controls), FIRST-ADMIN row, README tables, Hungarian freeze (`--add-app`). + ## wger hidden until R-762 and R-763 are fixed (2026-10-01, late afternoon) - Operator ruling 2026-10-01: `templates/wger/.felhom.yml` gets `lifecycle: hidden` — not offered for new installs; an diff --git a/FIRST-ADMIN.md b/FIRST-ADMIN.md index 848bd69..3a50495 100644 --- a/FIRST-ADMIN.md +++ b/FIRST-ADMIN.md @@ -70,6 +70,7 @@ asks the probe first where there is one. Open sign-up is closed by the box after | plex | 2 | the household's plex.tv claim token | – | fine | R | | privatebin | 5 | anonymous pastes by design | – | fine | R | | **radarr** | 4 | first visitor sets auth | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); no sign-up to close (single user / no accounts); `/initialize.json` hands anyone the API key BEFORE the setup — the gate hides it; after, it needs a login | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | +| **radicale** | 1 | the box generates the password at install; the container writes Radicale's login file from it on the FIRST start only (Radicale 3 ships with `auth = denyall`) | — (no first-run screen, no sign-up route) | **NEW 2026-10-01** — catalog, onboarding record `onboarding/radicale.md` | **M 9202** (drill catalog): no login 401 from the first answer; wrong 401; right 207; 15 wrong tries → right at once (no lock, ~1.3 s delay each); remove + restore → the household's login comes back with the data (`felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/`) | | **rallly** | 4 | magic link to any e-mail | **setup gate**, the household's press; magic link by e-mail — not provable on 9202 | **GATED** — catalog, 2026-09-29; the opening NOT proven | **M 9202**: stranger → gate page / 401, household reached the first-setup screen (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | | **recipe-importer** | 4 | our own image, open until set | **setup gate**, opened by the household's press („Kész, beállítottam", confirm first); no sign-up to close (single user / no accounts); our own app: open until a password is set in its settings — the confirm says so | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | | **romm** | 4 | first unauthenticated `POST /api/users` creates the user (harness); the stale `admin / admin` note is **removed** (2026-09-29) | **setup gate**, opened by probe `/api/heartbeat` → `SYSTEM.SHOW_SETUP_WIZARD` = false; the app itself refuses a stranger's second first-admin / sign-up call | **GATED** — catalog, 2026-09-29 (decisions 46–47) | **M 9202**: stranger → gate page / 401; household reached the first-setup screen; gate opened after the setup; the app answered after (`felhom.eu/documentation/audits/gate-rollout-2026-09-29/`) | diff --git a/README.md b/README.md index 23cbc42..2e33afd 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ block + the matching compose `${VAR}` lines. | Plex | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | plex.* | | PrivateBin | None (file) | 30M / 128M | yes | -- | paste.* | | Radarr | None (file) | 150M / 512M | yes | `${HDD_PATH}/media/` | radarr.* | +| Radicale | None (file) | 40M / 128M | yes | -- | calendar.* | | Rallly | PostgreSQL | 50M / 256M | yes | -- | poll.* | | RomM | MariaDB + Redis | 300M / 1024M | no | `${HDD_PATH}/storage/romm/` | arcade.* | | Jellyseerr | None (file) | 100M / 384M | yes | -- | requests.* | @@ -378,6 +379,7 @@ block + the matching compose `${VAR}` lines. | Plex | yes | yes | PLEX_CLAIM | | PrivateBin | yes | -- | -- | | Radarr | yes | yes | -- | +| Radicale | yes | -- | RADICALE_PASSWORD (password) | | Rallly | yes | -- | SECRET_PASSWORD, DB_PASSWORD | | RomM | yes | yes | DB_PASSWORD, MYSQL_ROOT_PASSWORD, ROMM_AUTH_SECRET_KEY | | Jellyseerr | yes | -- | -- | diff --git a/onboarding/radicale.md b/onboarding/radicale.md new file mode 100644 index 0000000..c5496cc --- /dev/null +++ b/onboarding/radicale.md @@ -0,0 +1,73 @@ +# Onboarding record — radicale + +app: radicale +opened: 2026-10-01 +template_at: the commit that publishes this record (ghcr.io/kozea/radicale:3.8.1; the step 3.8.0 -> 3.8.1 proven on both venues) + + + +0.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — GPL-3.0; the project's own image, pulled, never redistributed +0.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — v3.8.1 on 2026-09-24, 13 releases in 2026, 10 open issues +0.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md — `3.8.0`, `3.8.1` version tags; amd64, arm64, arm/v7 +0.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — no start-time network job (the start command is ours, read in the compose); no phone-home found in the image's config or code +0.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — healthy 22 s after the press with nothing fetched but the image +0.6 | n/a | Radicale serves CalDAV and CardDAV over plain HTTP on one port +0.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — a phone client's discovery through traefik over https: well-known 301, PROPFIND 207, the principal and the calendar list; no setup gate stands in the way +0.8 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — tagline + use_cases: the family calendar and contacts on its own server +0.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — runs on the bench; no public name is read +1.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — image-pins and image-resolvable exit 0 +1.2 | n/a | Radicale stores files, no database engine anywhere +1.3 | n/a | no MariaDB or PostgreSQL service in this template +1.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/step.txt — the previous release 3.8.0 seeded, stepped INTO the pin 3.8.1, read back on both venues (file storage; no migration switch exists) +1.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — PID 1 is `radicale` itself, the project's own server +1.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — the only secret is the login; no image config ships and auth defaults to `denyall`; the template sets htpasswd + bcrypt from the generated password +1.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — the image's entrypoint is `radicale --hosts …` with no switches; the template's own start command is the whole start-up +1.8 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — an unknown page answers 401 with no debug output +1.9 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — the password is NOT a data_key: the login file is written once and lives on the data volume, so a restore brings the old login back with the calendars (measured, 2.5); marking it would make a restore after a remove refuse +2.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt — the seed came back from the volume backup alone after the app and its volume were removed (the persistence question answered by a restore) +2.2 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — one named volume (NVMe): `collections/` + the login file +2.3 | n/a | needs_hdd false: no drive path to classify; the tier-1 unit holds the one volume +2.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — runs as uid 1000 `radicale`; the volume and the 0600 login file are its own +2.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt — the update's per-app backup, remove keeping backups, the household's restore button, the event read back with the ORIGINAL login (attempt 1 failed here: box/radicale-attempt1/restore.txt) +2.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/restore.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/remove.txt — keep-backups and with-data both delete the volume; no drive data exists +2.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — 92 KB after the seed; a family's calendars stay in megabytes +2.8 | n/a | Radicale stores no uploaded files: events and contacts are text, read back over CalDAV/CardDAV (2.5) +3.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — class 1: the box generates the password, the login works on a fresh install (C1) +3.2 | n/a | no known default login: without the template's config Radicale lets nobody in (`denyall`) +3.3 | n/a | no first-run screen: every request needs the login from the first answer +3.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — no sign-up route; a stranger's MKCOL on a new principal answers 401 +3.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/poll.txt — 25 stranger polls from the press: 404 until routed, then 401; never in +3.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — 15 wrong tries: no lock, ~1.3 s delay each (Radicale's own); the right password works at once +3.7 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — one shared login (no user admin in Radicale); `add_people` says each family member uses it and makes their own calendar. Changing the password is not offered in Radicale +3.8 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — the password is read from the environment inside the program, never pasted into code +3.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — CalDAV and CardDAV clients' calls through traefik: right 207/201, wrong 401; the browser UI logs in with the same pair (S2 screenshots) +4.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — wget is in the image (checked one tool per run); `127.0.0.1:5232/.web/` +4.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — probe-matches-compose exit 0 +4.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt — healthy 22 s after the press, 0 restarts +4.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — paused: the controller read `stopped`, then `running` after unpause +4.5 | done | app-catalog-felhom.eu/templates/radicale/docker-compose.yml — `container_name: radicale`, the only service +5.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/mem-radicale.csv — bench, swap 0, from birth: peak anon ~26 MiB of 128M, 0 kills +5.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — 10-min watch: anon peak ~20 %, 0 kills, 0 restarts +5.3 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — mem_limit 128M = the one service's 128M +5.4 | n/a | Radicale is a Python app, no self-sizing heap +5.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-radicale-reads.txt — pi_compatible true (arm64 + arm/v7); the image is ~97 MB +6.1 | done | app-catalog-felhom.eu/scripts/upgrade_fixtures_box.py — `Radicale`: a calendar and an event over CalDAV, with three negative controls +6.2 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — the first ladder step 3.8.0 -> 3.8.1, written by `upgrade-test.py --write-ladder` from both verdicts +6.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/undo/box/radicale/step.txt — a drill step to an image that exits: verifying 305 s, the box undid it by itself, 3.8.1 back, the event read back (on attempt 1's template; the undo is the box's, the start command played no part) +6.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json — no file changes at start (no mark) +6.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — plain `x.y.z` from v3.0 to v3.8.1 +7.1 | n/a | Radicale sends no mail at all; there is nothing to map +8.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — copy-i18n exit 0 (frozen with --add-app) +8.2 | done | app-catalog-felhom.eu/templates/radicale/.felhom.yml — tagline, use_cases, first_steps, add_people; each step followed on 9202 (the web login, MKCALENDAR, the phone discovery calls) +8.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-assets.txt — radicale-logo.svg and three screenshots answer 200 on felhom.eu; boxes get them with the next hub release (R-766) +8.4 | done | app-catalog-felhom.eu/README.md — both tables; FIRST-ADMIN row; category productivity; catalog_since +8.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-assets.txt — the website's count, read +9.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-radicale.txt — every gate exit 0, the runtime volume gate included +9.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/install.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/checks.txt — a fresh install from the drill catalog, as household and stranger +9.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/README.md — every row done or n/a; the one defect found (attempt 1, 2.5) was fixed before publishing +9.4 | done | app-catalog-felhom.eu/onboarding/radicale.md — published in one commit with this record (the onboarding gate) diff --git a/scripts/copy_freeze/hu.json b/scripts/copy_freeze/hu.json index a1d9188..ded9a4b 100644 --- a/scripts/copy_freeze/hu.json +++ b/scripts/copy_freeze/hu.json @@ -832,6 +832,27 @@ "deploy_fields[SUBDOMAIN].label": "Aldomain", "description": "Automatikus film letöltő és rendszerező" }, + "radicale": { + "app_info.add_people": "A Radicale-ban egy közös bejelentkezés van. A családtagod ugyanezzel a névvel és jelszóval veszi fel a telefonjára, és saját naptárat hoz létre magának.", + "app_info.first_steps[0]": "Nyisd meg a calendar.DOMAIN címet, és jelentkezz be a Beállítások oldalon látható névvel és jelszóval", + "app_info.first_steps[1]": "Hozz létre egy naptárat és egy címjegyzéket (\"Create new addressbook or calendar\")", + "app_info.first_steps[2]": "Androidon telepítsd a DAVx5-öt, és add meg: https://calendar.DOMAIN, a nevet és a jelszót", + "app_info.first_steps[3]": "iPhone-on: Beállítások, Naptár, Fiókok, Új fiók, Egyéb, CalDAV-fiók - szerver: calendar.DOMAIN", + "app_info.tagline": "Naptár és névjegyek a saját szervereden - a telefonod szinkronizál vele", + "app_info.use_cases[0]": "A telefonod naptára és névjegyei a saját szervereden, nem a Google-nél vagy az Apple-nél", + "app_info.use_cases[1]": "Közös családi naptár, amit mindenki lát a telefonján", + "app_info.use_cases[2]": "Androidon a DAVx5, iPhone-on és Macen a beépített naptár szinkronizál vele", + "app_info.use_cases[3]": "Thunderbird és más asztali naptárak is használhatják", + "deploy_fields[DOMAIN].description": "A szerver domain neve", + "deploy_fields[DOMAIN].label": "Domain", + "deploy_fields[RADICALE_PASSWORD].description": "Telepítéskor generált jelszó. A telefonod naptára és névjegyei is ezzel jelentkeznek be.", + "deploy_fields[RADICALE_PASSWORD].label": "Jelszó", + "deploy_fields[RADICALE_USER].description": "Ezzel a névvel jelentkezel be a telefonodon és a böngészőben", + "deploy_fields[RADICALE_USER].label": "Felhasználónév", + "deploy_fields[SUBDOMAIN].description": "Az alkalmazás aldomainje", + "deploy_fields[SUBDOMAIN].label": "Aldomain", + "description": "Naptár és névjegyek a saját szervereden (CalDAV / CardDAV)" + }, "rallly": { "app_info.first_steps[0]": "Nyisd meg a poll.DOMAIN címet a böngészőben", "app_info.first_steps[1]": "Hozz létre egy új szavazást", @@ -1162,5 +1183,7 @@ "description": "ShareX/Flameshot szerver - screenshot és fájlmegosztás" } }, - "reasons": {} + "reasons": { + "radicale": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules" + } } diff --git a/scripts/upgrade_fixtures_box.py b/scripts/upgrade_fixtures_box.py index c45f9c4..cdfab81 100644 --- a/scripts/upgrade_fixtures_box.py +++ b/scripts/upgrade_fixtures_box.py @@ -1875,6 +1875,61 @@ class UptimeKuma: return found +# ============================================================================================= +class Radicale: + """Radicale (2026-10-01, new app through NEW-APP-CHECKLIST.md). THE FRONT DOOR is CalDAV itself, the route every + phone and desktop calendar uses: HTTP basic auth with the box's generated login (`RADICALE_USER`, default + "csalad"; `RADICALE_PASSWORD`). Seed: MKCALENDAR a calendar, PUT one event (a unique UID and SUMMARY); read back + with GET. Negative controls on every readback: no credentials 401, a wrong password 401, an event never created + 404 — so a read that says "found" cannot be an open door or a catch-all.""" + sub = "calendar" + + @staticmethod + def _creds(w): + g = w.GENERATED.get("radicale") or {} + return g.get("RADICALE_USER") or "csalad", g.get("RADICALE_PASSWORD") or "" + + def seed(self, w, sub, say): + if not w.wait_app(sub, "/.web/", want=("200",), tries=60): + self.tried = "/.web/ never answered 200" + return None + user, pw = self._creds(w) + cal = "felhom-" + secrets.token_hex(4) + uid = "ev-" + secrets.token_hex(6) + "@felhom" + summary = "Felhom teszt " + secrets.token_hex(3) + rc, code, _ = w.app_curl(sub, f"/{user}/{cal}/", "-u", f"{user}:{pw}", method="MKCALENDAR") + say(f" radicale: MKCALENDAR /{user}/{cal}/ http={code}") + if code != "201": + self.tried = f"MKCALENDAR -> {code}" + return None + ics = ("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//felhom//fixture//EN\r\nBEGIN:VEVENT\r\n" + f"UID:{uid}\r\nDTSTAMP:20261001T100000Z\r\nDTSTART:20261002T100000Z\r\nSUMMARY:{summary}\r\n" + "END:VEVENT\r\nEND:VCALENDAR\r\n") + rc, code, _ = w.app_curl(sub, f"/{user}/{cal}/{uid}.ics", "-u", f"{user}:{pw}", "-H", "Content-Type: text/calendar", + data=ics, method="PUT") + say(f" radicale: PUT event http={code}") + if code != "201": + self.tried = f"PUT event -> {code}" + return None + return {"user": user, "pw": pw, "cal": cal, "uid": uid, "summary": summary} + + def verify(self, w, sub, t, say): + if not w.wait_app(sub, "/.web/", want=("200",), tries=60): + say(" radicale: /.web/ never answered") + return False + path = f"/{t['user']}/{t['cal']}/{t['uid']}.ics" + rc, c_none, _ = w.app_curl(sub, path) + rc, c_wrong, _ = w.app_curl(sub, path, "-u", f"{t['user']}:{t['pw']}x") + rc, c_absent, _ = w.app_curl(sub, f"/{t['user']}/{t['cal']}/never-{secrets.token_hex(3)}.ics", "-u", f"{t['user']}:{t['pw']}") + if c_none != "401" or c_wrong != "401" or c_absent != "404": + say(f" radicale: READBACK UNUSABLE — no auth {c_none}, wrong password {c_wrong}, absent event {c_absent}") + return False + rc, code, out = w.app_curl(sub, path, "-u", f"{t['user']}:{t['pw']}") + ok = code == "200" and f"SUMMARY:{t['summary']}" in (out or "") + say(f" radicale: readback http={code} found={ok} (controls: no auth {c_none}, wrong {c_wrong}, absent {c_absent})") + return ok + + FIXTURES = { "uptime-kuma": UptimeKuma(), "crafty-controller": Crafty(), @@ -1911,4 +1966,5 @@ FIXTURES = { "wishlist": Wishlist(), "claper": Claper(), "calcom": Calcom(), + "radicale": Radicale(), } diff --git a/templates/radicale/.felhom.yml b/templates/radicale/.felhom.yml new file mode 100644 index 0000000..fc037fa --- /dev/null +++ b/templates/radicale/.felhom.yml @@ -0,0 +1,116 @@ +# ============================================================================= +# .felhom.yml - App metadata for felhom-controller +# ============================================================================= +# Radicale — the Radicale project's own image (ghcr.io/kozea/radicale). Onboarding record: onboarding/radicale.md +# (NEW-APP-CHECKLIST.md). First admin: CLASS 1 — the box generates the password at install; the container writes the +# login file from it on the first start only (see the compose header). No setup gate: there is no first-run screen, and a +# request without the right login answers 401 from the first second. + +# --- Display info (shown on dashboard) --- +display_name: "Radicale" +description: "Naptár és névjegyek a saját szervereden (CalDAV / CardDAV)" +category: "productivity" +subdomain: "calendar" +slug: "radicale" +# catalog_since: the date THIS repo last changed this app's pinned images. Any commit that +# changes an image: line must set this to the same day (see CLAUDE.md). +catalog_since: "2026-10-01" + +# --- Resource hints (displayed on deploy screen) --- +resources: + mem_request: "40M" + mem_limit: "128M" # one service: radicale 128M + pi_compatible: true + needs_hdd: false + +# --- Deploy wizard fields --- +deploy_fields: + - env_var: DOMAIN + label: "Domain" + type: domain + description: "A szerver domain neve" + locked_after_deploy: true + + - env_var: SUBDOMAIN + label: "Aldomain" + type: subdomain + default: "calendar" + required: true + locked_after_deploy: true + description: "Az alkalmazás aldomainje" + + - env_var: RADICALE_USER + label: "Felhasználónév" + type: text + default: "csalad" + required: true + locked_after_deploy: true + description: "Ezzel a névvel jelentkezel be a telefonodon és a böngészőben" + + - env_var: RADICALE_PASSWORD + label: "Jelszó" + type: password + generate: "password:24" + locked_after_deploy: true + description: "Telepítéskor generált jelszó. A telefonod naptára és névjegyei is ezzel jelentkeznek be." + +# --- Customer-facing info --- +app_info: + tagline: "Naptár és névjegyek a saját szervereden - a telefonod szinkronizál vele" + add_people: "A Radicale-ban egy közös bejelentkezés van. A családtagod ugyanezzel a névvel és jelszóval veszi fel a telefonjára, és saját naptárat hoz létre magának." + docs_url: "https://radicale.org/v3.html" + use_cases: + - 'A telefonod naptára és névjegyei a saját szervereden, nem a Google-nél vagy az Apple-nél' + - 'Közös családi naptár, amit mindenki lát a telefonján' + - 'Androidon a DAVx5, iPhone-on és Macen a beépített naptár szinkronizál vele' + - 'Thunderbird és más asztali naptárak is használhatják' + first_steps: + - 'Nyisd meg a calendar.DOMAIN címet, és jelentkezz be a Beállítások oldalon látható névvel és jelszóval' + - 'Hozz létre egy naptárat és egy címjegyzéket ("Create new addressbook or calendar")' + - 'Androidon telepítsd a DAVx5-öt, és add meg: https://calendar.DOMAIN, a nevet és a jelszót' + - 'iPhone-on: Beállítások, Naptár, Fiókok, Új fiók, Egyéb, CalDAV-fiók - szerver: calendar.DOMAIN' + +# --- Controller health probe (dials what the compose healthcheck dials) --- +healthcheck: + checks: + - type: api + port: 5232 + path: "/.web/" + expect: + status: 200 + +i18n: + en: + description: 'Calendar and contacts on your own server (CalDAV / CardDAV)' + app_info: + tagline: 'Calendar and contacts on your own server - your phone syncs with it' + add_people: "Radicale has one shared login. Your family member adds the same name and password on their phone and makes their own calendar." + use_cases: + - 'Your phone calendar and contacts on your own server, not with Google or Apple' + - 'A shared family calendar that everyone sees on their phone' + - 'On Android DAVx5, on iPhone and Mac the built-in calendar syncs with it' + - 'Thunderbird and other desktop calendars can use it too' + first_steps: + - 'Open calendar.DOMAIN and sign in with the name and password shown on the settings page' + - 'Create a calendar and an address book ("Create new addressbook or calendar")' + - 'On Android install DAVx5 and enter: https://calendar.DOMAIN, the name and the password' + - 'On iPhone: Settings, Calendar, Accounts, Add Account, Other, CalDAV account - server: calendar.DOMAIN' + deploy_fields: + - env_var: DOMAIN + label: 'Domain' + description: 'The server domain name' + - env_var: SUBDOMAIN + label: 'Subdomain' + description: 'The subdomain this app answers on' + - env_var: RADICALE_USER + label: 'User name' + description: 'You sign in with this name on your phone and in the browser' + - env_var: RADICALE_PASSWORD + label: 'Password' + description: 'Generated at install. Your phone calendar and contacts sign in with it too.' + +# update_ladder — the test record: one tested step per line, oldest first (JSON flow mappings, +# `09-update-architecture.md` §6.4 part 4). WRITTEN BY scripts/upgrade-test.py, never by hand; +# gated by scripts/check-test-record.py. An image: move without a proven entry here is refused. +update_ladder: + - {"from": {"radicale": "ghcr.io/kozea/radicale:3.8.0"}, "to": {"radicale": "ghcr.io/kozea/radicale:3.8.1"}, "digest": {"radicale": "sha256:54d9406cd30f9e206a9dd6d61dfac48da26b9afc37200e14f23beb8fafa7d11c"}, "verdict": "proven", "tested_at": "2026-10-01T14:47:56Z", "harness_version": 4, "evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/bench/radicale/evidence/MV-radicale/verdict.json", "box_evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/box/radicale/step.txt", "memory_peak_pct": 20.0, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 23.7} diff --git a/templates/radicale/docker-compose.yml b/templates/radicale/docker-compose.yml new file mode 100644 index 0000000..6699f2f --- /dev/null +++ b/templates/radicale/docker-compose.yml @@ -0,0 +1,64 @@ +# Radicale - Naptár és névjegyek (CalDAV / CardDAV szerver) +# Domain: ${SUBDOMAIN}.${DOMAIN} +# Database: None (file-based — every calendar and address book is a folder of .ics/.vcf files) +# RAM: ~40M (mem_limit: 128M) | Pi-compatible: Yes (amd64, arm64, arm/v7) +# +# Environment variables: +# DOMAIN - Your domain (e.g., demo-felhom.eu) +# RADICALE_USER - A bejelentkezési név (az űrlapon megadható, alapból "csalad") +# RADICALE_PASSWORD - A jelszó (telepítéskor generálva, az alkalmazás oldalán látható) +# +# The upstream image (ghcr.io/kozea/radicale, the Radicale project's own) ships NO config, and Radicale 3's +# default `auth type` is `denyall` — nobody can log in. So the start command writes the login file from the +# box's generated password (bcrypt, 0600, on the data volume) ON THE FIRST START ONLY, then starts Radicale with +# htpasswd auth. There is never a moment without a login (no install window, checklist 3.5). The password is +# read from the environment, never pasted into program code (checklist 3.8). +# FIRST START ONLY, measured 2026-10-01 on 9202: the box never puts a `type: password` value into a backup (an +# internet-reachable login; controller `PortableSecretEnvVars`) and makes a NEW one on a restore after a remove, +# expecting the app's own login to come back WITH ITS DATA. The first version rewrote the file at every start — +# a remove + restore then replaced the household's login with a value no page shows, and every phone lost its +# calendar. Written once, the login file lives on the data volume and comes back with the calendars. + +services: + radicale: + image: ghcr.io/kozea/radicale:3.8.1 + container_name: radicale + restart: unless-stopped + environment: + - TZ=Europe/Budapest + - RADICALE_USER=${RADICALE_USER} + - RADICALE_PASSWORD=${RADICALE_PASSWORD} + entrypoint: + - /bin/sh + - -c + - | + [ -s /var/lib/radicale/users ] || /app/bin/python -c 'import os, bcrypt; f = "/var/lib/radicale/users"; u = os.environ["RADICALE_USER"]; p = os.environ["RADICALE_PASSWORD"].encode(); open(f, "w").write(u + ":" + bcrypt.hashpw(p, bcrypt.gensalt()).decode() + "\n"); os.chmod(f, 0o600)' || exit 1 + exec /app/bin/python /app/bin/radicale --hosts 0.0.0.0:5232 --auth-type htpasswd --auth-htpasswd-filename /var/lib/radicale/users --auth-htpasswd-encryption bcrypt --storage-filesystem-folder /var/lib/radicale/collections + volumes: + - radicale_data:/var/lib/radicale + networks: + - traefik-public + deploy: + resources: + limits: + memory: 128M + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:5232/.web/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + labels: + - "traefik.enable=true" + - "traefik.http.routers.radicale.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)" + - "traefik.http.routers.radicale.entrypoints=websecure" + - "traefik.http.routers.radicale.tls=true" + - "traefik.http.routers.radicale.tls.certresolver=letsencrypt" + - "traefik.http.services.radicale.loadbalancer.server.port=5232" + +volumes: + radicale_data: + +networks: + traefik-public: + external: true