Grimmory tested but held (R-775): its fixture added; CHANGELOG, CONTEXT, REPORT for the night's new apps
gates / gates (push) Successful in 2s
gates / gates (push) Successful in 2s
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:
@@ -1,3 +1,11 @@
|
||||
## Grimmory — tested, NOT published; fixture added (2026-10-01, night)
|
||||
|
||||
- Grimmory (the BookLore successor) passed the checklist on the bench and 9202 except one finding that needs the
|
||||
operator: a stranger's 5 wrong sign-ins lock EVERY visitor out for 15 minutes (hard-coded; one address behind the
|
||||
tunnel) — `felhom.eu` R-775. The template is NOT in `templates/`; it waits in
|
||||
`felhom.eu/documentation/audits/new-apps-2026-10-01/wip/grimmory/`. Its fixture `Grimmory` (first admin, a library, an
|
||||
uploaded EPUB, three negative controls) is in `upgrade_fixtures_box.py`, so the publishing session reuses it.
|
||||
|
||||
## Dawarich — the third app through the new-app checklist (2026-10-01, evening)
|
||||
|
||||
- **`templates/dawarich/`** — your own location history (instead of Google Timeline): `freikin/dawarich:1.15.3` (web +
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
> Created with the REUSE.md rollout (2026-07-03). History: `CHANGELOG.md`; format spec: `README.md`.
|
||||
|
||||
- **2026-10-01 (night) — FIRST NEW APPS through the checklist:** Radicale `195129c`, Karakeep `882ac14`, Dawarich `72247a3`
|
||||
published with complete records; Grimmory tested but held (R-775, operator); Grimoire/MeTube/Pinchflat stopped on the
|
||||
fit check; Invidious/moonlight-web fit only (`felhom.eu/documentation/audits/new-apps-2026-10-01/FIT.md`).
|
||||
- **Ruling 2026-10-01 (operator) — wger HIDDEN** (`lifecycle: hidden`) until R-762 and R-763 are fixed; the fix session
|
||||
un-hides it.
|
||||
- **2026-10-01 (evening) — THE NEW-APP CHECKLIST.** `NEW-APP-CHECKLIST.md` (60 rows, `since` per id) + `onboarding/`
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# REPORT — the new-app checklist: in the catalog, a gate, piloted on wger (2026-10-01, evening)
|
||||
# REPORT — the first new apps through the checklist (2026-10-01, night)
|
||||
|
||||
Full session report: `felhom.eu/REPORT-new-app-checklist-2026-10-01.md`; evidence
|
||||
`felhom.eu/documentation/audits/new-app-checklist-2026-10-01/`.
|
||||
Full session report: `felhom.eu/REPORT-new-apps-2026-10-01.md`; evidence `felhom.eu/documentation/audits/new-apps-2026-10-01/`
|
||||
(fit table `FIT.md`).
|
||||
|
||||
- **`NEW-APP-CHECKLIST.md`** reviewed: 60 rows in 10 groups (draft 53); 7 added, 16 sharpened, 9 wrong claims fixed.
|
||||
- **`onboarding/_TEMPLATE.md`** (one line per id) and **`onboarding/wger.md`** (the pilot; 11 open rows, each a register row).
|
||||
- **Gate `onboarding`** (`scripts/check-onboarding.py`, `--fast`: hook + CI). The 53 published apps are exempt by name.
|
||||
16 decoys judged right; 5 deliberately broken versions of the gate each turned the decoy suite red.
|
||||
- **`onboarding/EXISTING-APPS-GAPS.md`** from `scripts/onboarding_gaps.py` (read only).
|
||||
- **The pilot:** the draft caught R-752 and R-755 as written; it missed R-737 (its "how" logged in on the web form only)
|
||||
and R-738 for a NEW app (nothing to update at the newest tag). Rows 1.4/1.6 sharpened, 1.7/3.9 added — now all four.
|
||||
The new rows then found three live wger defects: R-762, R-763, R-764. No template was changed.
|
||||
- Gates: `catalog_gates.py --fast` OK (11); `test_gate_decoys.py` 121 OK; `test_catalog_gates.py` OK;
|
||||
`decoy_coverage_gate.py` 0 unaccounted.
|
||||
- **wger hidden** (`lifecycle: hidden`, `55b8c8a`) until R-762 and R-763 are fixed; read back on 9202: not on the app list.
|
||||
- **Published, each with a complete `onboarding/<app>.md` and its first proven ladder step (bench swap 0 + 9202):**
|
||||
- **Radicale** (`195129c`) — CalDAV/CardDAV, upstream image; the login file is written on the FIRST start only
|
||||
(R-765: rewritten at every start, a restore after a remove locked the household out — fixed before publishing).
|
||||
- **Karakeep** (`882ac14`) — bookmarks + crawler (Chrome) + search; AI off unless the household enters a key; setup gate,
|
||||
sign-up closed twice; web memory raised to 1536M on measurements.
|
||||
- **Dawarich** (`72247a3`) — location history, PostGIS 17; the seeded known login replaced by `after_install` behind the
|
||||
install hold; `SECRET_KEY_BASE` a data_key.
|
||||
- **Grimmory not published:** a stranger's 5 wrong sign-ins lock every visitor out for 15 min (hard-coded, one address
|
||||
behind the tunnel) — R-775, the operator's word. Its template waits in the evidence `wip/grimmory/`; its fixture
|
||||
(`Grimmory`) is in `upgrade_fixtures_box.py`.
|
||||
- **Not built on the fit check:** Grimoire, MeTube, Pinchflat; Invidious and moonlight-web fit only.
|
||||
- Fixtures added: `Radicale`, `Karakeep`, `Dawarich`, `Grimmory`. Gates: `catalog_gates.py --fast` OK on every push; the
|
||||
runtime volume gate CLEAN for each published app (bench).
|
||||
|
||||
@@ -2066,6 +2066,99 @@ class Dawarich:
|
||||
return found
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
class Grimmory:
|
||||
"""Grimmory (2026-10-01, new app through NEW-APP-CHECKLIST.md). THE FRONT DOOR is the app's own API, the one its web
|
||||
client calls: the first admin by `POST /api/v1/setup` (through the setup gate on the box, as the household), sign in
|
||||
with `POST /api/v1/auth/login` (a bearer token), a library on `/books`, and a real EPUB (built here, with a unique
|
||||
title) uploaded with `POST /api/v1/files/upload` — the app writes it into the household's book folder. Read back:
|
||||
`GET /api/v1/books` lists the title. Negative controls on every readback: no token 401, a wrong password 401, and a
|
||||
second `POST /api/v1/setup` refused (403) — the readback is not an open door."""
|
||||
sub = "library"
|
||||
|
||||
@staticmethod
|
||||
def _epub(title):
|
||||
import io as _io, zipfile
|
||||
b = _io.BytesIO()
|
||||
z = zipfile.ZipFile(b, "w")
|
||||
z.writestr(zipfile.ZipInfo("mimetype"), "application/epub+zip")
|
||||
z.writestr("META-INF/container.xml", '<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>')
|
||||
z.writestr("OEBPS/content.opf", f'<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf" unique-identifier="id" version="2.0"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>{title}</dc:title><dc:creator>Felhom</dc:creator><dc:identifier id="id">{title}</dc:identifier><dc:language>hu</dc:language></metadata><manifest><item id="c1" href="c1.xhtml" media-type="application/xhtml+xml"/></manifest><spine><itemref idref="c1"/></spine></package>')
|
||||
z.writestr("OEBPS/c1.xhtml", '<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><head><title>c</title></head><body><p>Szia!</p></body></html>')
|
||||
z.close()
|
||||
return b.getvalue()
|
||||
|
||||
def _token(self, w, sub, user, pw):
|
||||
rc, code, out = w.app_curl(sub, "/api/v1/auth/login", "-H", "Content-Type: application/json",
|
||||
data=json.dumps({"username": user, "password": pw}), method="POST")
|
||||
try:
|
||||
return code, json.loads(out).get("accessToken")
|
||||
except Exception:
|
||||
return code, None
|
||||
|
||||
def seed(self, w, sub, say):
|
||||
import tempfile
|
||||
if not w.wait_app(sub, "/api/v1/healthcheck", want=("200",), tries=90):
|
||||
self.tried = "/api/v1/healthcheck never answered 200"
|
||||
return None
|
||||
user, pw = "admin", "Gm-" + secrets.token_hex(10)
|
||||
rc, code, out = w.app_curl(sub, "/api/v1/setup", "-H", "Content-Type: application/json",
|
||||
data=json.dumps({"username": user, "password": pw, "email": "admin@felhom.invalid", "name": "Felhom"}), method="POST")
|
||||
say(f" grimmory: POST /api/v1/setup (the first admin) http={code}")
|
||||
if code != "200":
|
||||
self.tried = f"setup -> {code} {(out or '')[:120]}"
|
||||
return None
|
||||
code, tok = self._token(w, sub, user, pw)
|
||||
if not tok:
|
||||
self.tried = f"login -> {code}"
|
||||
return None
|
||||
auth = ["-H", f"Authorization: Bearer {tok}"]
|
||||
rc, code, out = w.app_curl(sub, "/api/v1/libraries", *auth, "-H", "Content-Type: application/json",
|
||||
data=json.dumps({"name": "Konyvek", "icon": "book", "iconType": "LUCIDE", "paths": [{"path": "/books"}],
|
||||
"watch": True, "metadataSource": "EMBEDDED", "organizationMode": "BOOK_PER_FILE"}), method="POST")
|
||||
try:
|
||||
lib = json.loads(out)
|
||||
lid, pid = lib["id"], lib["paths"][0]["id"]
|
||||
except Exception:
|
||||
self.tried = f"create library -> {code} {(out or '')[:120]}"
|
||||
return None
|
||||
title = "Felhom Teszt " + secrets.token_hex(4)
|
||||
fn = tempfile.mktemp(suffix=".epub")
|
||||
open(fn, "wb").write(self._epub(title))
|
||||
if hasattr(w, "put_file"):
|
||||
fn = w.put_file(fn)
|
||||
rc, code, out = w.app_curl(sub, "/api/v1/files/upload", *auth, "-F", f"file=@{fn};type=application/epub+zip",
|
||||
"-F", f"libraryId={lid}", "-F", f"pathId={pid}", method="POST")
|
||||
say(f" grimmory: library {lid}, upload of an EPUB http={code}")
|
||||
if code not in ("200", "201", "204"):
|
||||
self.tried = f"upload -> {code} {(out or '')[:120]}"
|
||||
return None
|
||||
return {"user": user, "pw": pw, "title": title}
|
||||
|
||||
def verify(self, w, sub, t, say):
|
||||
if not w.wait_app(sub, "/api/v1/healthcheck", want=("200",), tries=90):
|
||||
say(" grimmory: /api/v1/healthcheck never answered")
|
||||
return False
|
||||
rc, c_none, _ = w.app_curl(sub, "/api/v1/books")
|
||||
c_wrong, _ = self._token(w, sub, t["user"], t["pw"] + "x")
|
||||
rc, c_setup, _ = w.app_curl(sub, "/api/v1/setup", "-H", "Content-Type: application/json",
|
||||
data=json.dumps({"username": "x" + secrets.token_hex(2), "password": "Gm-" + secrets.token_hex(8),
|
||||
"email": "x@felhom.invalid", "name": "X"}), method="POST")
|
||||
if c_none != "401" or c_wrong != "401" or c_setup != "403":
|
||||
say(f" grimmory: READBACK UNUSABLE — no token {c_none}, wrong password {c_wrong}, second setup {c_setup}")
|
||||
return False
|
||||
code, tok = self._token(w, sub, t["user"], t["pw"])
|
||||
found = False
|
||||
for _ in range(12):
|
||||
rc, code, out = w.app_curl(sub, "/api/v1/books", "-H", f"Authorization: Bearer {tok}")
|
||||
found = code == "200" and t["title"] in (out or "")
|
||||
if found:
|
||||
break
|
||||
time.sleep(5)
|
||||
say(f" grimmory: readback http={code} found={found} (controls: no token {c_none}, wrong {c_wrong}, second setup {c_setup})")
|
||||
return found
|
||||
|
||||
|
||||
FIXTURES = {
|
||||
"uptime-kuma": UptimeKuma(),
|
||||
"crafty-controller": Crafty(),
|
||||
@@ -2105,4 +2198,5 @@ FIXTURES = {
|
||||
"radicale": Radicale(),
|
||||
"karakeep": Karakeep(),
|
||||
"dawarich": Dawarich(),
|
||||
"grimmory": Grimmory(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user