Karakeep: the second new app through the checklist — template, record, fixture, first ladder step 0.33.1 -> 0.33.2
gates / gates (push) Successful in 2s

Bookmarks/articles/notes with a crawler (karakeep-chrome) and search (meilisearch v1.41.0). AI off unless the household
enters a key; setup gate + sign-up closed twice; Chrome healthcheck over bash /dev/tcp; smtp_mapping (plaintext), mail-off
boot measured; web memory 768M -> 1536M on measurements (bench watch, box crawl burst). onboarding/karakeep.md complete.

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-01 18:01:07 +02:00
parent 195129cbb1
commit 882ac14309
8 changed files with 449 additions and 0 deletions
+14
View File
@@ -1,3 +1,17 @@
## Karakeep — the second app through the new-app checklist (2026-10-01, evening)
- **`templates/karakeep/`** — bookmarks, articles, notes (formerly Hoarder): `ghcr.io/karakeep-app/karakeep:0.33.2` +
`karakeep-chrome:151.0.7922.47-r1` (the crawler's browser) + `getmeili/meilisearch:v1.41.0` (as upstream's compose).
AI tagging OFF unless the household enters an OpenAI key at install (the field says what that sends where); release
check off; meilisearch analytics off. First admin class 4: setup gate, then sign-up closed twice (`after_setup` →
`DISABLE_SIGNUPS`, and a case-insensitive block of `/signup` + any tRPC call naming `users.create`, batched too).
Chrome has no wget/curl, so its healthcheck asks DevTools over bash's /dev/tcp. App-email by `smtp_mapping`
(plaintext :2526); mail OFF boots (measured).
- **Memory measured into the definition:** web 768M → 79 % in the bench watch; 1024M → 51 %, but real crawls on the box
reached 80.3 %; **1536M** → a 10-page burst peaked at 54 %. mem_limit 2816M.
- First ladder step 0.33.1 -> 0.33.2 proven on the bench (swap 0) and on 9202; remove + restore read back.
- `onboarding/karakeep.md`, fixture `Karakeep`, FIRST-ADMIN rows, README tables, Hungarian freeze.
## Radicale — the first app through the new-app checklist (2026-10-01, evening) ## 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 - **`templates/radicale/`** — calendar and contacts (CalDAV/CardDAV), the Radicale project's own image
+2
View File
@@ -55,6 +55,7 @@ asks the probe first where there is one. Open sign-up is closed by the box after
| homepage | 5 | static start page | – | fine | R | | homepage | 5 | static start page | – | fine | R |
| **immich** | 4 | first visitor admin sign-up | **setup gate**, probe `GET /api/server/config` → `isInitialized` | **GATED** — catalog, 2026-09-29 (decision 46) | **M 9202**: stranger 302→gate page / 401 during and after the install; household in 0.2 s; probe opened it 0–20 s after the sign-up; phone-app API (Bearer) 200 after (`felhom.eu/documentation/audits/login-gate-2026-09-29/C/`) | | **immich** | 4 | first visitor admin sign-up | **setup gate**, probe `GET /api/server/config` → `isInitialized` | **GATED** — catalog, 2026-09-29 (decision 46) | **M 9202**: stranger 302→gate page / 401 during and after the install; household in 0.2 s; probe opened it 0–20 s after the sign-up; phone-app API (Bearer) 200 after (`felhom.eu/documentation/audits/login-gate-2026-09-29/C/`) |
| **jellyfin** | 4 | startup wizard | **setup gate**, opened by probe `/System/Info/Public` → `StartupWizardCompleted`; 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/`) | | **jellyfin** | 4 | startup wizard | **setup gate**, opened by probe `/System/Info/Public` → `StartupWizardCompleted`; 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/`) |
| **karakeep** | 4 | the first account that signs up becomes the admin (`users.create`) | **setup gate**, opened by the household's press („Kész, beállítottam"); sign-up closed twice after the setup: `after_setup` → `DISABLE_SIGNUPS=true` (it also refuses the FIRST account, so never at install) + a case-insensitive block of `/signup` and any tRPC call naming `users.create` (batched too) | **NEW 2026-10-01** — catalog, `onboarding/karakeep.md` | **M 9202** (drill catalog): stranger 401 before the press; after it users.create / batched / upper-case / `//` / `/signup` all 403; 12 wrong key exchanges → the right one at once; after remove + restore the app's own switch still refuses, the route block is gone (R-773) (`felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/`) |
| kimai | 1 | `ADMIN_PASSWORD` → `ADMINPASS` | – | fine | R | | kimai | 1 | `ADMIN_PASSWORD` → `ADMINPASS` | – | fine | R |
| **komga** | 4 | first visitor claims | **setup gate**, opened by probe `/api/v1/claim` → `isClaimed`; 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/`) | | **komga** | 4 | first visitor claims | **setup gate**, opened by probe `/api/v1/claim` → `isClaimed`; 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/`) |
| **mealie** | 3 | `changeme@example.com / MyPassword` | (b) its own user repository (`update_password`), password as `sys.argv[1]` | **FIXED** — catalog, 2026-09-29 | **M 9202**: fresh install — default 401, generated 200, wrong 401 (`felhom.eu/documentation/audits/login-gate-2026-09-29/D/`) | | **mealie** | 3 | `changeme@example.com / MyPassword` | (b) its own user repository (`update_password`), password as `sys.argv[1]` | **FIXED** — catalog, 2026-09-29 | **M 9202**: fresh install — default 401, generated 200, wrong 401 (`felhom.eu/documentation/audits/login-gate-2026-09-29/D/`) |
@@ -105,6 +106,7 @@ Evidence: `felhom.eu/documentation/audits/signup-lock-2026-09-29/`.
| sparkyfitness | `SPARKY_FITNESS_DISABLE_SIGNUP` | yes | `/api/auth/sign-up/*` | all refused | | sparkyfitness | `SPARKY_FITNESS_DISABLE_SIGNUP` | yes | `/api/auth/sign-up/*` | all refused |
| termix | `ALLOW_REGISTRATION` | yes | `/users/create` — **its router ignores case**: `/users/CREATE` reached the sign-up before the block was made case-insensitive (its own switch refused it) | all refused | | termix | `ALLOW_REGISTRATION` | yes | `/users/create` — **its router ignores case**: `/users/CREATE` reached the sign-up before the block was made case-insensitive (its own switch refused it) | all refused |
| vikunja | `VIKUNJA_SERVICE_ENABLEREGISTRATION` | yes (its register route answers 404) | `/api/v1/register` | all refused (its `/register` PAGE is only HTML; the API is blocked) | | vikunja | `VIKUNJA_SERVICE_ENABLEREGISTRATION` | yes (its register route answers 404) | `/api/v1/register` | all refused (its `/register` PAGE is only HTML; the API is blocked) |
| karakeep | `DISABLE_SIGNUPS` | yes | `/signup`, tRPC `users.create` (any batch, any case) | all refused (2026-10-01) |
| opengist | none reachable (an admin setting in its database) | — | `/-/register` | all refused (block only) | | opengist | none reachable (an admin setting in its database) | — | `/-/register` | all refused (block only) |
| wishlist | none reachable (`system_config.enableSignup` in its database) | — | `/signup` | all refused (block only) | | wishlist | none reachable (`system_config.enableSignup` in its database) | — | `/signup` | all refused (block only) |
| wanderer | `PUBLIC_DISABLE_SIGNUP` (web only — PocketBase's API ignores it) | — | `/register` + PocketBase `POST /api/collections/(users\|_pb_users_auth_)/records`, any case — **the collection id and a case change both got in before the fix** | all refused; the batch API is off by the app | | wanderer | `PUBLIC_DISABLE_SIGNUP` (web only — PocketBase's API ignores it) | — | `/register` + PocketBase `POST /api/collections/(users\|_pb_users_auth_)/records`, any case — **the collection id and a case change both got in before the fix** | all refused; the batch API is off by the app |
+2
View File
@@ -305,6 +305,7 @@ block + the matching compose `${VAR}` lines.
| Homepage | None (file) | 50M / 256M | yes | -- | home.* | | Homepage | None (file) | 50M / 256M | yes | -- | home.* |
| Immich | PostgreSQL + Redis | 2048M / 4096M | no | `${HDD_PATH}/storage/immich/` | photos.* | | Immich | PostgreSQL + Redis | 2048M / 4096M | no | `${HDD_PATH}/storage/immich/` | photos.* |
| Jellyfin | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | media.* | | Jellyfin | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | media.* |
| Karakeep | SQLite + Meilisearch + Chrome | 700M / 2816M | no | -- | bookmarks.* |
| Kimai | MariaDB | 100M / 384M | yes | -- | time.* | | Kimai | MariaDB | 100M / 384M | yes | -- | time.* |
| Komga | None (file) | 200M / 512M | yes | `${HDD_PATH}/media/comics/` | comics.* | | Komga | None (file) | 200M / 512M | yes | `${HDD_PATH}/media/comics/` | comics.* |
| Mealie | None (SQLite) | 200M / 1000M | yes | -- | recipes.* | | Mealie | None (SQLite) | 200M / 1000M | yes | -- | recipes.* |
@@ -364,6 +365,7 @@ block + the matching compose `${VAR}` lines.
| Homepage | yes | -- | -- | | Homepage | yes | -- | -- |
| Immich | yes | yes | DB_PASSWORD | | Immich | yes | yes | DB_PASSWORD |
| Jellyfin | yes | yes | -- | | Jellyfin | yes | yes | -- |
| Karakeep | yes | -- | NEXTAUTH_SECRET, MEILI_MASTER_KEY, OPENAI_API_KEY (optional, secret_input) |
| Kimai | yes | -- | DB_PASSWORD, ADMIN_EMAIL, ADMIN_PASSWORD | | Kimai | yes | -- | DB_PASSWORD, ADMIN_EMAIL, ADMIN_PASSWORD |
| Komga | yes | yes | -- | | Komga | yes | yes | -- |
| Mealie | yes | -- | -- | | Mealie | yes | -- | -- |
+73
View File
@@ -0,0 +1,73 @@
# Onboarding record — karakeep
app: karakeep
opened: 2026-10-01
template_at: the commit that publishes this record (karakeep 0.33.2 + karakeep-chrome 151.0.7922.47-r1 + meilisearch v1.41.0; web 1536M)
<!--
Evidence root: felhom.eu/documentation/audits/new-apps-2026-10-01/. bench/karakeep-768M/ and box/karakeep-768M/ are the
first definition (web 768M); bench/karakeep/ and box/karakeep/ the second (1024M, the proven ladder step);
box/karakeep-final/ the final definition (1536M) and its crawl burst. The web limit went 768M -> 1024M -> 1536M on
measurements (5.1, 5.2, 5.4); a limit that only rises cannot make a proven step fail.
-->
0.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — AGPL-3.0; upstream's own images, pulled
0.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/A1-github-facts.txt — 0.33.2 on 2026-08-11; 5 releases in 2026; pushed 2026-09-29
0.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md — `0.33.1`, `0.33.2`, chrome `151.0.7922.47-r1`, meilisearch `v1.41.0`; amd64 + arm64
0.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — AI tagging OFF (no OPENAI_API_KEY; the log says „No inference client configured"); release check off (`DISABLE_NEW_RELEASE_CHECK`); meilisearch analytics off; the crawler fetches each saved page and an ad-block list (the product); the deploy field says what turning AI on sends where; the official phone app has crash reporting built in (FIT.md, R-774 — not on the page yet)
0.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep-768M/neg-and-crawl.txt — crawling needs the internet (example.com crawled with a screenshot); a private address is refused by Karakeep itself (felhom.eu resolves to the LAN here)
0.6 | n/a | Karakeep serves HTTP on port 3000 only; Chrome and Meilisearch stay on the internal network
0.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — the phone app's sign-in route (`apiKeys.exchange`) through traefik after the setup gate opened: right 200 + a key that reads the API, wrong 401
0.8 | done | app-catalog-felhom.eu/templates/karakeep/.felhom.yml — tagline + use_cases
0.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep/evidence/MV-karakeep/verdict.json — runs on the bench; NEXTAUTH_URL builds links only
1.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-karakeep.txt — image-pins and image-resolvable exit 0
1.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/A/compose/karakeep.yml — SQLite inside the app, meilisearch v1.41.0 exactly as upstream's compose
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/karakeep/evidence/MV-karakeep/verdict.json ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/step.txt — 0.33.1 seeded, stepped INTO 0.33.2; the image's s6 `init-db-migration` service ran; read back on both venues
1.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — s6 supervises Next.js' production server and the workers (the image's own init)
1.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-karakeep-reads.txt — NEXTAUTH_SECRET and MEILI_MASTER_KEY generated; OPENAI_API_KEY empty by default; the rest of the key-like settings belong to features off here (OAuth, Prometheus)
1.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-karakeep-reads.txt — the entrypoint is s6 (`/init`) with fixed services; migrations run as its own `init-db-migration` service at every start; no switch to decide
1.8 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — an unknown page redirects (308), no debug output
1.9 | n/a | NEXTAUTH_SECRET signs sessions only (sign in again), MEILI_MASTER_KEY guards a rebuildable index; neither encrypts stored data
2.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/cvp-karakeep-tail.txt — volume-persistence gate CLEAN
2.2 | done | app-catalog-felhom.eu/templates/karakeep/docker-compose.yml — two named volumes (NVMe): the data (SQLite + assets) and the search index
2.3 | n/a | needs_hdd false: no drive path to classify; the tier-1 unit holds both volumes
2.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — the app wrote its database and crawled assets on first start
2.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/restore.txt — the update's per-app backup, remove keeping backups, the household's restore button, the bookmark read back with the household's own email + password
2.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/restore.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep-768M/remove.txt — both remove choices delete both volumes; no drive data exists
2.7 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — 732 KB + 512 KB after the seed and one crawl; each saved page adds its screenshot and content (MBs over a year)
2.8 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep-768M/neg-and-crawl.txt — the crawled page's screenshot is stored as an asset and served back through the app (assets: 2); the UI shows it (shots/karakeep/1.png)
3.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/install.txt — class 4: the first account became admin through the gate as the household
3.2 | n/a | no known default login: the first account is the household's own
3.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — `setup_gate`, opened by the household's press; before it a stranger's GET and sign-up answered 401; no probe (Karakeep has no public "set up" status)
3.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — after the press: users.create, batched, upper-case, `//` and `/signup` all 403; the app's own switch DISABLE_SIGNUPS=true. After remove + restore the switch still refuses, the route block is gone (R-773)
3.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/poll.txt — 93 stranger polls from the press: 404 until routed, then the gate's 401; never the app
3.6 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — 12 wrong sign-ins for the household's email: 401 each, no lock; the right one at once
3.7 | done | app-catalog-felhom.eu/templates/karakeep/.felhom.yml — `add_people`: the admin creates the family member's account (sign-up is closed); password change is in Karakeep's own settings
3.8 | n/a | no after_install command: the household makes its own first account
3.9 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/shots/karakeep/1.png — the API route (phone app, browser extension) and the browser sign-in over https through traefik (headless Chrome signed in)
4.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-karakeep-reads.txt — web: the image's own wget check (127.0.0.1); chrome: bash + raw HTTP/1.1 to DevTools (no wget/curl in that image); meilisearch: curl/wget /health
4.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-karakeep.txt — probe-matches-compose exit 0
4.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/install.txt — all three healthy 83 s after the press (pulls included), 0 restarts
4.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — the app container stopped: `degraded` within 10 s, front door 404; back in 15 s. (A paused container reads running — Docker's count; the probe that found no container recorded healthy, R-772)
4.5 | done | app-catalog-felhom.eu/templates/karakeep/docker-compose.yml — `container_name: karakeep`; sidecars `karakeep-chrome`, `karakeep-meilisearch`
5.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep/mem-karakeep.csv — bench, swap 0, from birth: web ~503 MiB, chrome 35, meili 31; 0 kills
5.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep-final/burst.txt — at 1536M, 10 real pages saved at once: web 832 MiB = 54 %, chrome 28 %, meili 17 %, 0 kills, 0 restarts (at 1024M a crawl reached 80.3 %: box/karakeep/remove.txt)
5.3 | done | app-catalog-felhom.eu/templates/karakeep/.felhom.yml — mem_limit 2816M = 1536 + 768 + 512
5.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep-768M/evidence/MV-karakeep/verdict.json ; felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep/evidence/MV-karakeep/verdict.json — two watches at two limits: 79 % of 768M, 51 % of 1024M — it does not simply fill the limit
5.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S1-karakeep-reads.txt — pi_compatible false (Chrome + Meilisearch); ~2 GB of images
6.1 | done | app-catalog-felhom.eu/scripts/upgrade_fixtures_box.py — `Karakeep`: the first account, the phone app's key exchange, a bookmark over the REST API, three negative controls
6.2 | done | app-catalog-felhom.eu/templates/karakeep/.felhom.yml — the first ladder step 0.33.1 -> 0.33.2, written by `upgrade-test.py --write-ladder`
6.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/undo/box/radicale/step.txt — the box's own undo, proven on a real failed step this session (Radicale); Karakeep's step ran the same guarded Update (backing-up, verifying, done)
6.4 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep/evidence/MV-karakeep/verdict.json — no mark
6.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md — plain `x.y.z` app tags; chrome tags follow Chrome's version; `release`/`latest` are not used
7.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep-mail-off-boot.txt — `smtp_mapping` (plaintext :2526); a fresh install with mail OFF (empty SMTP_*) boots and signs up. Mail ON is not provable on 9202 (no hub) — R-774
8.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-karakeep.txt — copy-i18n exit 0 (frozen with --add-app)
8.2 | done | app-catalog-felhom.eu/templates/karakeep/.felhom.yml — tagline, use_cases, first_steps, add_people, the AI sentence on the deploy field; the first steps followed on 9202 (account, „Kész" press, API key)
8.3 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-karakeep-assets.txt — logo + three screenshots answer 200; boxes get them with the next hub release (R-766)
8.4 | done | app-catalog-felhom.eu/README.md — both tables; FIRST-ADMIN rows; category productivity; catalog_since
8.5 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/S/S3-karakeep-assets.txt — the website's count holds
9.1 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/G/gates-karakeep.txt — every gate exit 0, the runtime volume gate included
9.2 | done | felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/install.txt ; felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/checks.txt — fresh installs 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; findings are rows R-772, R-773, R-774
9.4 | done | app-catalog-felhom.eu/onboarding/karakeep.md — published in one commit with this record (the onboarding gate)
+22
View File
@@ -490,6 +490,27 @@
"deploy_fields[SUBDOMAIN].label": "Aldomain", "deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Ingyenes és nyílt forráskódú média szerver" "description": "Ingyenes és nyílt forráskódú média szerver"
}, },
"karakeep": {
"app_info.add_people": "A családtagodat te hozod létre: Beállítások, Admin, Felhasználók, Új felhasználó.",
"app_info.first_steps[0]": "Nyisd meg a bookmarks.DOMAIN címet, és hozd létre a fiókodat (az első fiók lesz az admin)",
"app_info.first_steps[1]": "Kattints a „Kész, beállítottam\" gombra az alkalmazás oldalán - ezután más már nem regisztrálhat",
"app_info.first_steps[2]": "Telepítsd a böngészőbővítményt vagy a telefonos alkalmazást, és add meg: https://bookmarks.DOMAIN és egy API-kulcsot (Beállítások, API-kulcsok)",
"app_info.first_steps[3]": "Mentsd el az első könyvjelződet",
"app_info.tagline": "Könyvjelzők, cikkek és jegyzetek mentése - kereshető saját archívum",
"app_info.use_cases[0]": "Weboldalak mentése: a Karakeep letölti és elteszi a tartalmukat, akkor is, ha az oldal később eltűnik",
"app_info.use_cases[1]": "Jegyzetek, képek és PDF-ek egy helyen, teljes szöveges kereséssel",
"app_info.use_cases[2]": "Böngészőbővítmény és telefonos alkalmazás a gyors mentéshez",
"app_info.use_cases[3]": "AI-címkézés csak akkor, ha te megadsz hozzá egy OpenAI-kulcsot - alapból ki van kapcsolva",
"deploy_fields[DOMAIN].description": "A szerver domain neve",
"deploy_fields[DOMAIN].label": "Domain",
"deploy_fields[MEILI_MASTER_KEY].label": "Keresőindex-kulcs",
"deploy_fields[NEXTAUTH_SECRET].label": "Munkamenet-kulcs",
"deploy_fields[OPENAI_API_KEY].description": "Üresen hagyva az automatikus AI-címkézés ki van kapcsolva, és semmi nem megy ki a szerveredről. Ha megadod, minden új könyvjelző szövege az OpenAI-hoz kerül címkézésre.",
"deploy_fields[OPENAI_API_KEY].label": "OpenAI API-kulcs (nem kötelező)",
"deploy_fields[SUBDOMAIN].description": "Az alkalmazás aldomainje",
"deploy_fields[SUBDOMAIN].label": "Aldomain",
"description": "Könyvjelzők, cikkek, jegyzetek és képek egy helyen, kereshetően"
},
"kimai": { "kimai": {
"app_info.default_creds": "(telepítéskor megadott admin email és jelszó)", "app_info.default_creds": "(telepítéskor megadott admin email és jelszó)",
"app_info.first_steps[0]": "Nyisd meg a time.DOMAIN címet a böngészőben", "app_info.first_steps[0]": "Nyisd meg a time.DOMAIN címet a böngészőben",
@@ -1184,6 +1205,7 @@
} }
}, },
"reasons": { "reasons": {
"karakeep": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules",
"radicale": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules" "radicale": "new app 2026-10-01 through NEW-APP-CHECKLIST.md: te-form, no kérjük; reviewed by CC against the operator rules"
} }
} }
+74
View File
@@ -1930,6 +1930,79 @@ class Radicale:
return ok return ok
# =============================================================================================
class Karakeep:
"""Karakeep (2026-10-01, new app through NEW-APP-CHECKLIST.md). THE FRONT DOOR is the app's own: the first account
is made by its sign-up call (tRPC `users.create` — the first account becomes the admin; through the setup gate on the
box, as the household), then the route its phone app uses to sign in — `apiKeys.exchange` (email + password -> an API
key) — and the REST API: `POST /api/v1/bookmarks` (a text bookmark with a unique title), read back with
`GET /api/v1/bookmarks/<id>`. Negative controls on every readback: no key 401, a wrong key 401, an id never made 404.
A text bookmark, not a URL: the crawler's success depends on the internet, the read-back must not."""
sub = "bookmarks"
@staticmethod
def _trpc(w, sub, proc, payload):
rc, code, out = w.app_curl(sub, f"/api/trpc/{proc}?batch=1", "-H", "Content-Type: application/json",
data=json.dumps({"0": {"json": payload}}), method="POST")
try:
return code, json.loads(out)[0]
except Exception:
return code, {"_raw": (out or "")[:200]}
def _key(self, w, sub, t):
code, r = self._trpc(w, sub, "apiKeys.exchange", {"keyName": "felhom-" + secrets.token_hex(3),
"email": t["email"], "password": t["pw"]})
return ((r.get("result") or {}).get("data") or {}).get("json", {}).get("key") if code == "200" else None
def seed(self, w, sub, say):
if not w.wait_app(sub, "/api/health", want=("200",), tries=72):
self.tried = "/api/health never answered 200"
return None
email = "admin-" + secrets.token_hex(3) + "@felhom.invalid"
pw = "Kk-" + secrets.token_hex(10)
code, r = self._trpc(w, sub, "users.create", {"name": "Felhom", "email": email, "password": pw, "confirmPassword": pw})
role = ((r.get("result") or {}).get("data") or {}).get("json", {}).get("role")
say(f" karakeep: users.create (the first account) http={code} role={role}")
if code != "200":
self.tried = f"users.create -> {code} {str(r)[:120]}"
return None
t = {"email": email, "pw": pw}
key = self._key(w, sub, t)
if not key:
self.tried = "apiKeys.exchange gave no key"
return None
title = "felhom-seed-" + secrets.token_hex(4)
rc, code, out = w.app_curl(sub, "/api/v1/bookmarks", "-H", f"Authorization: Bearer {key}", "-H", "Content-Type: application/json",
data=json.dumps({"type": "text", "text": "Felhom teszt jegyzet " + title, "title": title}), method="POST")
say(f" karakeep: POST /api/v1/bookmarks http={code}")
try:
bid = json.loads(out)["id"]
except Exception:
self.tried = f"POST bookmark -> {code} {(out or '')[:120]}"
return None
t.update({"id": bid, "title": title, "role": role})
return t
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/api/health", want=("200",), tries=72):
say(" karakeep: /api/health never answered")
return False
key = self._key(w, sub, t)
if not key:
say(" karakeep: the household's email + password no longer give a key")
return False
rc, c_none, _ = w.app_curl(sub, f"/api/v1/bookmarks/{t['id']}")
rc, c_wrong, _ = w.app_curl(sub, f"/api/v1/bookmarks/{t['id']}", "-H", "Authorization: Bearer ak1_felhom_wrong")
rc, c_absent, _ = w.app_curl(sub, "/api/v1/bookmarks/never" + secrets.token_hex(4), "-H", f"Authorization: Bearer {key}")
if c_none != "401" or c_wrong != "401" or c_absent != "404":
say(f" karakeep: READBACK UNUSABLE — no key {c_none}, wrong key {c_wrong}, absent id {c_absent}")
return False
rc, code, out = w.app_curl(sub, f"/api/v1/bookmarks/{t['id']}", "-H", f"Authorization: Bearer {key}")
ok = code == "200" and t["title"] in (out or "")
say(f" karakeep: readback http={code} found={ok} (controls: no key {c_none}, wrong {c_wrong}, absent {c_absent})")
return ok
FIXTURES = { FIXTURES = {
"uptime-kuma": UptimeKuma(), "uptime-kuma": UptimeKuma(),
"crafty-controller": Crafty(), "crafty-controller": Crafty(),
@@ -1967,4 +2040,5 @@ FIXTURES = {
"claper": Claper(), "claper": Claper(),
"calcom": Calcom(), "calcom": Calcom(),
"radicale": Radicale(), "radicale": Radicale(),
"karakeep": Karakeep(),
} }
+135
View File
@@ -0,0 +1,135 @@
# =============================================================================
# .felhom.yml - App metadata for felhom-controller
# =============================================================================
# Karakeep (formerly Hoarder) — ghcr.io/karakeep-app/karakeep. Onboarding record: onboarding/karakeep.md.
# First admin: CLASS 4 — the first account that signs up becomes the admin. So: the setup gate (decision 46) keeps a
# fresh install closed to everyone but the household until it says „Kész, beállítottam"; then the box closes sign-up
# twice (decision 47): the app's own switch (after_setup → DISABLE_SIGNUPS, which would refuse the FIRST account too, so
# never at install) and a block of the sign-up routes.
# --- Display info (shown on dashboard) ---
display_name: "Karakeep"
description: "Könyvjelzők, cikkek, jegyzetek és képek egy helyen, kereshetően"
category: "productivity"
subdomain: "bookmarks"
slug: "karakeep"
# 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: "700M"
mem_limit: "2816M" # karakeep 1536M + karakeep-chrome 768M + karakeep-meilisearch 512M = 2816M
pi_compatible: false
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: "bookmarks"
required: true
locked_after_deploy: true
description: "Az alkalmazás aldomainje"
- env_var: NEXTAUTH_SECRET
label: "Munkamenet-kulcs"
type: secret
generate: "hex:32"
locked_after_deploy: true
- env_var: MEILI_MASTER_KEY
label: "Keresőindex-kulcs"
type: secret
generate: "hex:32"
locked_after_deploy: true
- env_var: OPENAI_API_KEY
label: "OpenAI API-kulcs (nem kötelező)"
type: secret_input
required: false
description: "Üresen hagyva az automatikus AI-címkézés ki van kapcsolva, és semmi nem megy ki a szerveredről. Ha megadod, minden új könyvjelző szövege az OpenAI-hoz kerül címkézésre."
setup_gate: true
signup_block: "PathRegexp(`(?i)^/+(signup|api/trpc/[^?]*users\\.create)`)"
after_setup:
env:
SIGNUP_CLOSED: "true"
# --- App-email (password reset) through the box's mail relay; plaintext :2526 (see the compose comment) ---
smtp_mapping:
tls_mode: plaintext
host_var: SMTP_HOST
port_var: SMTP_PORT
from_var: SMTP_FROM
from_local: karakeep
# --- Customer-facing info ---
app_info:
tagline: "Könyvjelzők, cikkek és jegyzetek mentése - kereshető saját archívum"
add_people: "A családtagodat te hozod létre: Beállítások, Admin, Felhasználók, Új felhasználó."
docs_url: "https://docs.karakeep.app/"
use_cases:
- 'Weboldalak mentése: a Karakeep letölti és elteszi a tartalmukat, akkor is, ha az oldal később eltűnik'
- 'Jegyzetek, képek és PDF-ek egy helyen, teljes szöveges kereséssel'
- 'Böngészőbővítmény és telefonos alkalmazás a gyors mentéshez'
- 'AI-címkézés csak akkor, ha te megadsz hozzá egy OpenAI-kulcsot - alapból ki van kapcsolva'
first_steps:
- 'Nyisd meg a bookmarks.DOMAIN címet, és hozd létre a fiókodat (az első fiók lesz az admin)'
- 'Kattints a „Kész, beállítottam" gombra az alkalmazás oldalán - ezután más már nem regisztrálhat'
- 'Telepítsd a böngészőbővítményt vagy a telefonos alkalmazást, és add meg: https://bookmarks.DOMAIN és egy API-kulcsot (Beállítások, API-kulcsok)'
- 'Mentsd el az első könyvjelződet'
# --- Controller health probe (dials what the compose healthcheck dials) ---
healthcheck:
checks:
- type: api
port: 3000
path: "/api/health"
expect:
status: 200
i18n:
en:
description: 'Bookmarks, articles, notes and images in one place, searchable'
app_info:
tagline: 'Save bookmarks, articles and notes - your own searchable archive'
add_people: "You create your family member's account: Settings, Admin, Users, New user."
use_cases:
- 'Save web pages: Karakeep downloads and keeps their content, even if the page disappears later'
- 'Notes, images and PDFs in one place, with full-text search'
- 'A browser extension and a phone app for quick saving'
- 'AI tagging only if you give it an OpenAI key - off by default'
first_steps:
- 'Open bookmarks.DOMAIN and create your account (the first account becomes the admin)'
- 'Press "Done, I set it up" on the app page - after that nobody else can sign up'
- 'Install the browser extension or the phone app and enter: https://bookmarks.DOMAIN and an API key (Settings, API keys)'
- 'Save your first bookmark'
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: NEXTAUTH_SECRET
label: 'Session key'
- env_var: MEILI_MASTER_KEY
label: 'Search index key'
- env_var: OPENAI_API_KEY
label: 'OpenAI API key (optional)'
description: 'Left empty, automatic AI tagging is off and nothing leaves your server. If you enter one, the text of every new bookmark goes to OpenAI for tagging.'
# 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": {"karakeep": "ghcr.io/karakeep-app/karakeep:0.33.1", "karakeep-chrome": "ghcr.io/karakeep-app/karakeep-chrome:151.0.7922.47-r1", "karakeep-meilisearch": "getmeili/meilisearch:v1.41.0"}, "to": {"karakeep": "ghcr.io/karakeep-app/karakeep:0.33.2", "karakeep-chrome": "ghcr.io/karakeep-app/karakeep-chrome:151.0.7922.47-r1", "karakeep-meilisearch": "getmeili/meilisearch:v1.41.0"}, "digest": {"karakeep": "sha256:b069e4307dec06ea06d16989c6861c30a1ff208568be44ed5fb5d422cd3e950c", "karakeep-chrome": "sha256:5b19bbb160e9ff60681a3abd97e1c4ec9f64212301410de658c3900ab7ef31e7", "karakeep-meilisearch": "sha256:860fa4baed04ae1c235de870edab0c8006227546dea1bbb6411fbfc5e27cf1db"}, "verdict": "proven", "tested_at": "2026-10-01T15:36:10Z", "harness_version": 4, "evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/bench/karakeep/evidence/MV-karakeep/verdict.json", "box_evidence": "felhom.eu/documentation/audits/new-apps-2026-10-01/box/karakeep/step.txt", "memory_peak_pct": 51.4, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 55.4}
+127
View File
@@ -0,0 +1,127 @@
# Karakeep - Könyvjelzők, cikkek és jegyzetek mentése (a korábbi Hoarder)
# Domain: ${SUBDOMAIN}.${DOMAIN}
# Database: SQLite (in the data volume) + Meilisearch (search index) + a headless Chrome (the crawler)
# RAM: ~700M (mem_limit: 2816M total — web 1536M + chrome 768M + meilisearch 512M) | Pi-compatible: No (Chrome + Meilisearch)
#
# Environment variables:
# DOMAIN - Your domain (e.g., demo-felhom.eu)
# NEXTAUTH_SECRET - Munkamenet-aláíró kulcs (generált)
# MEILI_MASTER_KEY - A keresőindex kulcsa (generált; csak a belső hálón használt)
# OPENAI_API_KEY - NEM KÖTELEZŐ. Üresen az AI-címkézés KI van kapcsolva (Karakeep: `isConfigured = !!OPENAI_API_KEY ||
# !!OLLAMA_BASE_URL`). Ha a háztartás megadja, a könyvjelzők szövege az OpenAI-hoz kerül.
# SIGNUP_CLOSED - after_setup (decision 47): the box sets it when the household's first account exists. Karakeep's
# DISABLE_SIGNUPS also refuses the FIRST account, so the default here must stay OPEN.
#
# Mirrors upstream's docker/docker-compose.yml (web + karakeep-chrome + meilisearch v1.41.0, MEILI_NO_ANALYTICS),
# pinned, with the new-release check off (it would call api.github.com) and the crawler's own defaults kept: it fetches
# every bookmarked page — that IS the product — and an ad-block list at start (CRAWLER_ENABLE_ADBLOCKER).
services:
karakeep:
image: ghcr.io/karakeep-app/karakeep:0.33.2
container_name: karakeep
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- DATA_DIR=/data
- NEXTAUTH_URL=https://${SUBDOMAIN}.${DOMAIN}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- MEILI_ADDR=http://karakeep-meilisearch:7700
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
- BROWSER_WEB_URL=http://karakeep-chrome:9222
- DISABLE_NEW_RELEASE_CHECK=true
- DISABLE_SIGNUPS=${SIGNUP_CLOSED:-false}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
# App-email (smtp_mapping, tls_mode plaintext → the shim's :2526, which advertises no STARTTLS: Karakeep's mailer
# has no switch to accept the shim's self-signed certificate). Empty SMTP_HOST = mail OFF; measured 2026-10-01 that a
# fresh install with these EMPTY boots and signs up (checklist 7.1).
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_FROM=${SMTP_FROM:-}
volumes:
- karakeep_data:/data
networks:
- traefik-public
- karakeep-internal
depends_on:
karakeep-meilisearch:
condition: service_healthy
# 1536M (measured 2026-10-01): the web container also runs the crawler workers. Bench 10-minute watch: 79 % of 768M,
# 51 % of 1024M; on the box, real pages crawled (a Wikipedia article among them) took it to 822 MiB = 80.3 % of 1024M.
# So the household's ordinary act — saving a heavy page — needs room; the burst is re-measured at 1536M (record 5.2).
deploy:
resources:
limits:
memory: 1536M
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:3000/api/health || curl -fsS http://127.0.0.1:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
labels:
- "traefik.enable=true"
- "traefik.http.routers.karakeep.rule=Host(`${SUBDOMAIN}.${DOMAIN}`)"
- "traefik.http.routers.karakeep.entrypoints=websecure"
- "traefik.http.routers.karakeep.tls=true"
- "traefik.http.routers.karakeep.tls.certresolver=letsencrypt"
- "traefik.http.services.karakeep.loadbalancer.server.port=3000"
karakeep-chrome:
image: ghcr.io/karakeep-app/karakeep-chrome:151.0.7922.47-r1
container_name: karakeep-chrome
restart: unless-stopped
init: true
command:
- --disable-gpu
- --disable-dev-shm-usage
- --hide-scrollbars
- --disable-blink-features=AutomationControlled
- --window-size=1440,900
networks:
- karakeep-internal
deploy:
resources:
limits:
memory: 768M
# The image has no wget/curl/node (inspected one tool per run, 2026-10-01) but has bash: ask Chrome's own DevTools
# endpoint (socat on 9222 → headless-shell on 9223) for its version over a raw HTTP/1.1 request (HTTP/1.0 got no
# answer — measured 2026-10-01 on the bench).
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/9222 && printf 'GET /json/version HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n' >&3 && grep -q Browser <&3"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
karakeep-meilisearch:
image: getmeili/meilisearch:v1.41.0
container_name: karakeep-meilisearch
restart: unless-stopped
environment:
- TZ=Europe/Budapest
- MEILI_NO_ANALYTICS=true
- MEILI_MASTER_KEY=${MEILI_MASTER_KEY}
volumes:
- karakeep_meili:/meili_data
networks:
- karakeep-internal
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:7700/health || wget -q -O - http://127.0.0.1:7700/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
volumes:
karakeep_data:
karakeep_meili:
networks:
traefik-public:
external: true
karakeep-internal: