diff --git a/CHANGELOG.md b/CHANGELOG.md index d50d0ff..f8d57af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - **`templates/radicale/`** — calendar and contacts (CalDAV/CardDAV), the Radicale project's own image diff --git a/FIRST-ADMIN.md b/FIRST-ADMIN.md index 3a50495..8223565 100644 --- a/FIRST-ADMIN.md +++ b/FIRST-ADMIN.md @@ -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 | | **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/`) | +| **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 | | **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/`) | @@ -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 | | 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) | +| 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) | | 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 | diff --git a/README.md b/README.md index 2e33afd..feb5fb3 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,7 @@ block + the matching compose `${VAR}` lines. | Homepage | None (file) | 50M / 256M | yes | -- | home.* | | Immich | PostgreSQL + Redis | 2048M / 4096M | no | `${HDD_PATH}/storage/immich/` | photos.* | | Jellyfin | None (file) | 512M / 2048M | no | `${HDD_PATH}/media/` | media.* | +| Karakeep | SQLite + Meilisearch + Chrome | 700M / 2816M | no | -- | bookmarks.* | | Kimai | MariaDB | 100M / 384M | yes | -- | time.* | | Komga | None (file) | 200M / 512M | yes | `${HDD_PATH}/media/comics/` | comics.* | | Mealie | None (SQLite) | 200M / 1000M | yes | -- | recipes.* | @@ -364,6 +365,7 @@ block + the matching compose `${VAR}` lines. | Homepage | yes | -- | -- | | Immich | yes | yes | DB_PASSWORD | | Jellyfin | yes | yes | -- | +| Karakeep | yes | -- | NEXTAUTH_SECRET, MEILI_MASTER_KEY, OPENAI_API_KEY (optional, secret_input) | | Kimai | yes | -- | DB_PASSWORD, ADMIN_EMAIL, ADMIN_PASSWORD | | Komga | yes | yes | -- | | Mealie | yes | -- | -- | diff --git a/onboarding/karakeep.md b/onboarding/karakeep.md new file mode 100644 index 0000000..6d06feb --- /dev/null +++ b/onboarding/karakeep.md @@ -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) + + + +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) diff --git a/scripts/copy_freeze/hu.json b/scripts/copy_freeze/hu.json index ded9a4b..3bf2296 100644 --- a/scripts/copy_freeze/hu.json +++ b/scripts/copy_freeze/hu.json @@ -490,6 +490,27 @@ "deploy_fields[SUBDOMAIN].label": "Aldomain", "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": { "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", @@ -1184,6 +1205,7 @@ } }, "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" } } diff --git a/scripts/upgrade_fixtures_box.py b/scripts/upgrade_fixtures_box.py index cdfab81..df98ccb 100644 --- a/scripts/upgrade_fixtures_box.py +++ b/scripts/upgrade_fixtures_box.py @@ -1930,6 +1930,79 @@ class Radicale: 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/`. 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 = { "uptime-kuma": UptimeKuma(), "crafty-controller": Crafty(), @@ -1967,4 +2040,5 @@ FIXTURES = { "claper": Claper(), "calcom": Calcom(), "radicale": Radicale(), + "karakeep": Karakeep(), } diff --git a/templates/karakeep/.felhom.yml b/templates/karakeep/.felhom.yml new file mode 100644 index 0000000..b15f97b --- /dev/null +++ b/templates/karakeep/.felhom.yml @@ -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} diff --git a/templates/karakeep/docker-compose.yml b/templates/karakeep/docker-compose.yml new file mode 100644 index 0000000..219d57e --- /dev/null +++ b/templates/karakeep/docker-compose.yml @@ -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: