76a5e92f2e
116 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 76a5e92f2e |
An instance had no way to look like itself: the top bar said "Dorfteich"
All checks were successful
whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`. |
|||
| f8c241b11a |
#304: custom fonts in the pickers, an admin screen, and the licence page
The backend from #303 could store an operator's font but nothing could choose one: no list endpoint outside the Site-Admin routes, no @font-face rules for a family that only exists at runtime, and no management UI. Found while wiring it up — a real defect in #303, invisible to its tests: `fontStack` cannot tell an uploaded family from a deleted one, so the PDF exporter embedded the face and then never named it. Every export of a pond using an operator font rendered in the system font while the job reported success. Both `fontStack` call sites now take the uploaded families (`buildPdfHtml`, `pondFontVariables`); `pdf-html.test.ts` pins the regression from both sides. Verified against a real Gotenberg: with the families the PDF embeds PlayfairDisplay-Bold, without them NotoSans-Bold — that was the whole bug, in one diff of two PDFs. - `GET /fonts/custom` is readable by any signed-in user, not Site Admins only: the pickers, the licence page and the injected `@font-face` rules all need it, and gating it would have forced a second, admin-only UI. - Bundled and uploaded families are told apart by their `<optgroup>`, not by a badge — the grouping is then part of the control's semantics, so a screen reader announces it and the native mobile select keeps it. Within each source the catalog's category grouping is preserved. - The delete confirmation names how many ponds use the family and what happens to them; focus moves to it and back on cancel. Deletion stays unblocked (the api's decision, #303) — the ponds degrade, they do not break. - The licence page grew a second table. That is what makes an attribution obligation satisfiable: a commercial licence that requires naming the foundry needs a page to name it on. Verified in the browser end to end (upload two weights → listed and rendered in its own font → chosen in a pond → page renders in it → deleted → pond falls back): api suite for fonts/export 77 passed, a11y pack 11/11 locally in both schemes, lint/typecheck/i18n:check green. |
|||
| 45f1925917 |
#302: configurable pond start page, created with every new pond
Opening a pond landed on whatever sorted first in the sidebar — stable, but a rule nobody could see, and one whose target moved as soon as someone added a page ahead of it. New ponds landed on the empty-pond hint instead of anything useful. - `startPageId` joins the pond settings. No migration: `Pond.settings` is already jsonb. It stores an id, not a slug, so renaming or moving the page keeps it working. - `PondHomePage` prefers it, but only when the page is in this user's page list. That list already holds just what they may see, so a start page hidden by a page-scoped grant — or trashed — falls back silently instead of landing them on a 404, and it costs no extra request. - Both creation paths give the pond a start page, titled from the creator's stored locale. It happens after the creating transaction commits: the owner's grant is written inside it and permissions cache per pond, so creating the page any earlier would ask about rights the grant has not published yet. A failure is logged, not fatal — a pond without a start page still works. `PagesModule` imported `PondsModule` without using it. Removing that vestigial edge let PondsModule depend on PagesModule in the honest direction instead of tying the two together with forwardRef. Every pond created through the api now owns a page, which broke eight suites whose teardown deleted ponds directly — `Page.pond` deliberately has no cascade, because a real purge removes contents explicitly and audits it. A shared `deletePondsWhere` helper deletes pages first. Two tests that counted pages now account for the start page rather than pretending the pond began empty. |
|||
| 5a4a99196e |
#300: route icon-only controls through IconButton/IconLink
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m36s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Build container images (pull_request) Successful in 3m51s
CD / Build and push images (push) Successful in 15s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m32s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m25s
CI / Import/export fidelity gate (push) Successful in 58s
The notification bell sat higher and larger than search and the theme toggle next to it. The cause was not the glyph: `.notifications-bell__button` carried its own rules with neither flex centring nor an icon size, so the svg was laid out inline on the text baseline and rendered at lucide's 24px default instead of the 1.15rem the shared `.icon-button` enforces. Route every icon-only control through the shared components instead: - `IconLink` joins `IconButton`, sharing one class helper. Three controls navigate (pond settings, graph, trash) and are links, not buttons — without a link twin they would have stayed the one group gluing the class on by hand. - 17 hand-applied `className="icon-button …"` usages across nine files now go through the components, which is what enforces the accessible name on a control that shows only an icon. - The bell's unread count reaches assistive technology. The badge sits inside the control, so `aria-label` hid it and a screen reader announced "Notifications" without ever saying how many. An ESLint rule keeps it that way: `icon-button` on a raw button, anchor or Link is now an error, in both string and template-literal form. The plugin uninstall button keeps a title that differs from its name (it explains why a required plugin is locked); IconButton spreads rest last, so the explicit title still wins. Also drops the graphify block from CLAUDE.md — it duplicates the workspace-level instructions. |
|||
| 9b7acab294 |
#232: plugin allowlist with SHA-256 hash pinning
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m21s
CI / Build container images (pull_request) Successful in 3m59s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m27s
CI / Import/export fidelity gate (push) Successful in 58s
CI / Lint, typecheck, test (push) Successful in 6m30s
The install path records the SHA-256 of the delivered bundle ZIP (plugins.bundle_hash; pre-#232 installs show it as unknown until reinstalled). plugins.allowlist in instance_settings names permitted ids with their pinned hashes: empty (default) = not enforced, existing instances unchanged; non-empty = installs of unlisted or deviating bundles are rejected (plugin_not_pinned / plugin_hash_mismatch, 403), and an installed plugin outside the list or with a deviating hash does not load — absent from pond mount lists, frame/assets 404. Every rejection is audited (plugin.rejected, catalogue v1.5). A version bump changes the hash and therefore requires an explicit re-pin — the intended friction (ADR 0025). Admin UI shows observed vs pinned hash per plugin with pin/re-pin/unpin. Scope stated honestly in plugin-architecture.md: the pin answers "is this the reviewed bundle"; post-install disk tampering is platform integrity (ADR 0019), sandbox containment stays the sandbox's job. Hardening guide row + catalog advisory triage; residual risk R-03 resolved. e2e: empty-allowlist compatibility, pinned load, unpinned and tampered installs rejected and audited, pin drift blocks loading while the admin still sees the mismatch, version bump needs re-pin. Full api suite 101 files / 561 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 4d9f913845 |
#246: mode enforced — reject profile-violating configuration writes
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m18s
CI / Build container images (pull_request) Successful in 4m3s
CI / Auth e2e pack (pull_request) Successful in 8m43s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 23s
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Deploy to Test (push) Successful in 12s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m27s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m24s
CI / Import/export fidelity gate (push) Successful in 58s
In enforced mode the ONE settings write path every caller uses rejects catalog-violating values with the stable code vs_nfd_profile_violation (403 — the request is well-formed, the policy says no). Existing violating values are reported at startup (log line, database-less boots must not fail) and on the admin card, never auto-changed. The UI renders as in hidden (#245 already keys on hidden|enforced). The hardening guide now names enforced as the recommended mode for VS-NfD reference operation. Tests: violating write rejected with the stable code and nothing stored; compliant writes pass; the same violating write passes in marked and hidden (own app boots); pre-existing violation reported and untouched. Full api suite 100 files / 555 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 0d95e1304e |
#245: mode hidden — hide profile-violating options, mark the hiding
All checks were successful
CI / Lint, typecheck, test (push) Successful in 6m24s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m34s
CI / Import/export fidelity gate (push) Successful in 1m1s
CI / Build container images (pull_request) Successful in 1m13s
CI / Lint, typecheck, test (pull_request) Successful in 6m14s
CI / Auth e2e pack (pull_request) Successful in 8m31s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
In hidden (and later enforced) mode, catalog-listed controls whose only purpose is enabling a violation are not rendered while their saved value is compliant (the four master switches, the Nextcloud backup block); value-listed selects keep only their compliant choices (registration mode, new-page classification, upload policy, SVG policy). Every affected section shows one accessible policy note (i18n de+en) so policy is distinguishable from missing features. A value that was already violating is surfaced exactly like in marked — never silently hidden. The API stays unchanged; enforcement is #246. e2e: hidden half of the marking pack (rows disappear, note visible, already-violating row stays marked, axe WCAG A/AA clean) — verified live locally; CI runs it against a second api (VS_NFD_MODE=hidden, same database) behind its own static server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 5fdef95f67 |
#244: mode marked — flag profile-violating configuration in the UI
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m34s
CI / Build container images (pull_request) Successful in 1m19s
CI / Auth e2e pack (pull_request) Successful in 8m23s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m49s
CI / Import/export fidelity gate (push) Successful in 58s
Every catalog-listed control on the admin surfaces carries an accessible deviation marking in mode marked: text + icon under the control (never colour alone), part of the control's accessible description (aria-describedby), i18n de+en. The check runs against the CURRENT control value, so a violating choice is marked before saving. Covered controls: registration mode, new-page classification, upload policy, SVG policy, the four master switches (api/mcp/feeds/plugins), the legal texts (violating while empty), and the Nextcloud backup toggle on the system panel. The profile card (#243) gains the warning summary and the hardening-guide reference. e2e: new vs-nfd-marking pack (marked half in CI — the e2e api now runs VS_NFD_MODE=marked, which also puts the marked state into the a11y admin scan; off half in local default runs; both halves verified live). hidden/enforced follow in #245/#246. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| da5fd7c770 |
#243: VS_NFD_MODE and the machine-readable hardening-profile catalog
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m12s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 8m29s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Build and push images (push) Successful in 31s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m29s
CD / Promote to Int (push) Successful in 14s
CI / Build container images (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 6m30s
CI / Auth e2e pack (push) Successful in 8m6s
CI / Import/export fidelity gate (push) Successful in 57s
The deployment declares through VS_NFD_MODE (off | marked | hidden | enforced, default off) how the application treats configuration that violates the VS-NfD reference profile — deploy-level like BACKUP_ALLOWED_TARGETS, so a compromised Site Admin cannot widen it. The catalog in shared (vs-nfd-profile.ts) is the single source of truth: every profile-relevant setting with a decidable compliant value, judgement calls in an explicit advisory list, and a fence test parsing the hardening guide's reference tables so neither can drift (pattern #201). The api evaluates the catalog against the typed settings registry and validated env and exposes mode + verdict on GET /admin/system/vs-nfd-profile; the admin settings view shows the card whenever the mode is not off. Display only — the treatments land with #244–#246 (ADR 0027, proposed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 5796b7a5dd |
#214: OIDC Authorization Code with PKCE, Keycloak as reference IdP
External authentication (ADR 0021) built on jose (#188's vetted library) plus fetch — no new dependency enters the supply chain for a security base function. Discovery-configured; ID tokens validate against the IdP's JWKS under an explicit RS256/ES256 allowlist with issuer, audience, expiry and nonce binding. State, nonce and the PKCE verifier travel in a signed HttpOnly Lax cookie keyed by a dedicated HKDF purpose (oidc-state, ADR 0020). Deploy-level configuration (OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPES/ PROVIDER_LABEL): who authenticates users is a platform decision. The login page discovers the provider via GET /auth/methods and renders the SSO button (i18n de+en). Identities use the existing slot (provider oidc:<issuer>, subject from the token). First login creates the account just-in-time — ACTIVE and mail-verified only when the IdP asserts a verified address. An existing local account is NEVER adopted silently by e-mail (account-takeover path): login refuses with oidc_link_required and the owner links explicitly via GET /auth/oidc/link (audited auth.identity_linked, catalogue v1.3). Sessions come from the one existing session service. Tests run the full flow against a protocol-faithful fake IdP: PKCE verifier at the token endpoint, JIT creation incl. personal pond, invalid state/nonce/signature/issuer/audience/expiry each rejected, the linking refusal and the explicit link flow. Verified end-to-end against a real Keycloak 26.0 (repeatable procedure documented in security.md §External authentication). Refs #214. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8 |
|||
| 868b79c8bc |
#213: warn on uploads to classified pages; instance policy can block
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
The attachments panel of a classified page shows a persistent notice naming the consequence (de+en): the file inherits the page's classification but its content carries no marking (#212). The new instance setting classification.uploadPolicy (default warn, documented; the VS-NfD reference configuration blocks, #227) hardens the warning into a server-side rejection (403 classified_upload_blocked) — enforced in the upload service, not only in the UI. Tests: warning visible in the local attachments pack; block enforced server-side with warn/block both ways and open pages unaffected. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| adceca7358 |
#206: show the VS-NfD marking in web view header and footer
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m24s
CI / Build container images (pull_request) Successful in 4m24s
CI / Auth e2e pack (pull_request) Successful in 8m44s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Build and push images (push) Successful in 26s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m30s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m55s
CI / Import/export fidelity gate (push) Failing after 50s
ClassificationBanner renders the fixed ADR-0022 wording above and below the content in reading view, editor and public page view; unclassified pages show nothing. Announced to assistive tech via a localized hidden prefix (de+en); styled from the plain text token only, so contrast holds in both themes and under every accent with no new color pair. Public content endpoint now carries the classification. New seed fixture classified-note; a11y pack asserts banner top+bottom and axe-clean in light and dark. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 488d0d06f1 |
#205: classification inherits down the tree; lowering is a guarded, audited act
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m40s
CI / Build container images (pull_request) Successful in 4m34s
CI / Auth e2e pack (pull_request) Successful in 9m7s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
New pages take max(instance default, parent level); moving a subtree under a higher-classified parent raises every member below that level. No move-like path (reposition, trash-promote, purge-promote) lowers a level as a side effect — pinned by test. Raising is ordinary editorial work; lowering requires the dedicated capability canLowerClassification (pond-wide Pond Admin) in the central permission model. Both directions are audited (page.classification_raised/_lowered, catalogue v1.1) with old value, new value, actor and page. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| 183faf7710 |
#204: classification as first-class page metadata (ADR 0022)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m42s
CI / Build container images (pull_request) Successful in 3m56s
CI / Auth e2e pack (pull_request) Successful in 8m17s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 24s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 6m17s
CD / Smoke tests against Test (push) Successful in 3m32s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 8m20s
CI / Import/export fidelity gate (push) Successful in 55s
Enum field on Page (UNCLASSIFIED default, VS_NFD), migration backfills existing pages. New pages take the instance-wide default from classification.newPageDefault (admin-visible, de+en). The value rides in every PageView, so no channel needs an extra request. The field is a marking, not a protection mechanism: a test pins that permission decisions are unchanged by it. The marking wording is fixed in ADR 0022 and sourced solely from classificationMarking() in @dorfteich/shared. Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com> |
|||
| c4c84b33f9 |
#200: hard instance-wide plugins.enabled kill switch
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m38s
CI / Build container images (pull_request) Successful in 4m11s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 1m9s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Failing after 51s
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 5m37s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
plugins.enabled (instance setting, default on — plugins predate the switch; the VS-NfD reference configuration turns it off) makes every plugin surface answer 404 via a shared guard: Site-Admin install/list/mode, pond activation and plugin list, the sandbox frame and asset routes. The dropzone watcher quarantines drops instead of installing. Deliberately NOT guarded: the authenticated fallback-metadata route — it serves no plugin code and existing plugin_block nodes need it to render their declared fallback (an image fallback degrades to the neutral placeholder while off, because its bytes live on the disabled asset surface). The editor offers no plugin blocks because the pond plugin list is one of the 404ing surfaces. Admin settings panel gets the toggle (i18n de+en) with the documented api-restart note (in-process settings cache). Answers "code execution inside the zone?" with one verifiable off-switch instead of per-plugin trust machinery (#232, ADR 0025). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ |
|||
| 394d1c811d |
#192: deploy-level backup target allowlist
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m54s
CI / Auth e2e pack (pull_request) Successful in 8m4s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 5m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m41s
CI / Import/export fidelity gate (push) Successful in 56s
BACKUP_ALLOWED_TARGETS (comma-separated destination hosts) constrains where backups may go, enforced twice: the api rejects settings writes and connection tests towards non-allowlisted hosts with admin-visible error codes and resolves a non-allowlisted configured target to null, and the sidecar enforces the same policy at the point of egress for the WebDAV upload and the rsync mirror alike (shared policy helpers in packages/shared/src/backup-target-policy.ts). BREAKING: the empty default disables every remote target - backups stay local only, the VS-NfD reference configuration (ADR 0026). Existing deployments with a remote target must list its host or uploads and mirror stop. The admin UI distinguishes unavailable-by-policy from unconfigured (i18n de+en) and shows the permitted hosts. Refs #192 (ADR 0026) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ |
|||
| afef45732a |
#191: feeds.enabled instance switch, feed-token log masking
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m52s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m55s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m34s
CI / Import/export fidelity gate (push) Successful in 58s
Chosen path: an instance master switch following the api.enabled/ mcp.enabled pattern — while off, both feed routes AND the feed-token management answer 404 (existence hidden). Default ON: feeds predate the switch, existing instances and their subscribed readers keep working; the VS-NfD reference configuration (#227) turns it off. Admin UI gets the toggle next to the API/MCP switches (i18n de+en). Moving the token out of the query string is documented as rejected: a path segment lands in the same proxy and request logs, and feed readers cannot send headers — that is why the credential is in the URL at all. What DID leak was our own request log (pino logs req.url): the req serializer now masks ?token= values (common/mask-token-param.ts), so no code path logs the credential. Refs #191 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ |
|||
| b5d2a436e0 |
#186: pond accent theming — scoped derivation, cascade pond > user > default
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m44s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 10m50s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 4m2s
CI / Lint, typecheck, test (push) Successful in 4m47s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 14s
CI / Auth e2e pack (push) Successful in 10m7s
CI / Import/export fidelity gate (push) Successful in 56s
Release / Build release images and notes (push) Successful in 1m11s
Release / Release-candidate operations QA (push) Successful in 1m0s
Prod deploy / Deploy the released images to Prod (push) Successful in 17s
pondSettingsSchema gains theme = { accent: '#rrggbb' | null } (null =
inherit the viewer's theme), exposed as a top-level key of the flat
updatePondInputSchema and included in the PondsService settings merge
(the known silent-no-op pitfall). The server validates only the hex;
conformance arises at render time: PondThemeScope (mounted around the
page content next to PondFontScope) derives the accent pair for the
EFFECTIVE mode via useEffectiveTheme and sets it as inline custom
properties — inline beats both tokens.css and the user-theme <style>,
which IS the cascade precedence pond > user > default.
Pond settings get a PondThemeSection (inherit | presets | custom color
with per-mode preview swatches, explicit save like the font manager);
AccentSwatches extracted for reuse; i18n de+en. The no-JS public shell
stays deliberately un-themed (ADR 0018 amendment).
Tests: pond DB test (theme merge keeps fonts, invalid hex 400), e2e
pond-theme.spec (scope boundary content vs. chrome, per-mode
re-derivation, axe on the pond settings page; resets the fixture pond).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
|
|||
| 83a2fe470e |
#184: user accent theming — presets and free color as one mechanism
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m41s
CI / Build container images (pull_request) Successful in 4m4s
CI / Auth e2e pack (pull_request) Successful in 11m40s
CI / Import/export fidelity gate (pull_request) Successful in 52s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
apply-theme.ts derives BOTH modes' accent tokens from the stored choice
(ui.theme.accent: preset id or {custom:'#hex'}) and writes them as
<style id="user-theme"> with :root:root + :root:root[data-theme='dark']
blocks — the doubled :root beats tokens.css regardless of document
order, since theme-init.js injects the ui.theme.css cache during <head>
parsing, before the bundle styles. The default preset means NO override
(hand-tuned tokens.css values stay). main.tsx re-derives from the
choice at startup, healing stale caches after app updates.
Settings: accent radiogroup inside the Appearance section (visible
names, color never the only cue) with per-mode preview swatches on
each mode's canonical background, plus a custom color input; i18n
de+en. The second fieldset made bare .settings-fieldset locators
ambiguous — theme specs now scope via input[name] (fence stays).
Tests: apply-theme unit pack, BASE_PALETTE<->tokens.css drift fence in
theme-contrast.test.ts, e2e theme-accent.spec (instant apply, pre-paint
persistence, default removes override, axe smoke with garish yellow in
both modes). ADR 0018 amendment documents the stage-B details.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
|
|||
| b799ad180b |
#182: top-bar theme toggle — cycle light/dark/system without a menu
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m40s
CI / Build container images (pull_request) Successful in 4m1s
CI / Auth e2e pack (pull_request) Successful in 8m30s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
An IconButton between the notifications bell and the user menu cycles the theme mode in radio order (sun/moon/monitor mirror the CURRENT choice). New useThemeMode() hook is the single write path (persist + apply + same-document event), so the settings radios and the toggle stay in sync; AppearanceSection now uses it too. Also rendered for signed-out visitors — the mode is a device-local preference. i18n de+en; unit tests for cycle/setter, theme.spec covers cycling, radio sync, persistence, and the signed-out top bar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX |
|||
| 2c571f9f5e |
#180: dark mode — Light/Dark/System setting with token-based dark palette
The dark palette lives as a single :root[data-theme='dark'] block in tokens.css; theme.ts and the pre-paint public/theme-init.js (external file because the prod CSP forbids inline scripts) always resolve the stored ui.theme.mode to a concrete data-theme, so 'system' needs no @media duplicate and follows live OS changes via matchMedia. color-scheme flips per theme (native controls/scrollbars), paired theme-color metas track the effective theme, and the new Appearance settings section offers the three-way choice as native radios (device-local, like #170). Label chips gain a chip-outline ring so arbitrary user colors stay separated on the dark canvas; useEffectiveTheme() is exported for the later pond-scoped theming stage (ADR 0018). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX |
|||
| 31b59f0fb6 |
#170: Statusmeldungen, Einzeltasten-Shortcuts, Bewegung
Toast-Standzeit 2,5s auf 6s (WCAG 2.2.1 — für Screenreader-/Zoom-Nutzer kaum erfassbar). Neue Einstellungs-Sektion Bedienung mit dem Schalter Einzeltasten-Kürzel deaktivieren (lokale Geräte-Einstellung); die Handler von e und / prüfen sie beim Tastendruck (WCAG 2.1.4). prefers-reduced-motion: CSS-Transitions kollabieren auf instant, die Graph-Simulation rechnet ihr Layout synchron zu Ende statt zu animieren (WCAG 2.2.2). settings-nav-Spec auf 8 Sektionen nachgeführt. Bewusst KEIN zusätzliches role=status (legal.spec-Locator-Falle). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 58c19abfdd |
#169: Nicht-Text-Inhalte — Task-Checkboxen, Wissensgraph
Task-Checkboxen tragen in beiden Renderpfaden einen Namen: docToHtml setzt aria-label aus dem Aufgabentext, die Editor-NodeView ebenso. Die NodeView rendert ihr Host-Element jetzt selbst als li (ReactNodeView- Renderer as/attrs) — TipTaps zusätzliches div-Host-Element zwischen ul und li brach die Listensemantik; der Wrapper flacht per display:contents ab, die #137-Pixel-Abstimmung bleibt erhalten (Selektor auf die neue Tiefe nachgeführt, Ausrichtung nachgemessen: 1px-Versatz unverändert). Der Wissensgraph-SVG bekommt ein beschreibendes aria-label inklusive Verweis auf die Backlinks als gleichwertige Listenform. Der Bild-Alt-Editor existierte bereits (Bild-Controls bei Auswahl) — kein Änderungsbedarf. Hinweis: gecachte Seiten übernehmen das Checkbox-Label wie bei jeder docToHtml-Änderung erst mit dem nächsten Persist ihrer Inhalte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 8719b0ee1e |
#168: Formulare — Fehler-Verdrahtung und Namenslücken
Der Field-Baustein verdrahtet Hinweis/Fehler jetzt per aria-describedby und aria-invalid mit dem Eingabefeld (cloneElement auf das einzelne Kind; Fragmente bleiben unangetastet) — Screenreader nennen den Fehler damit auch beim Feld-Fokus. Quota-Typ-Select mit Namen; die leeren Aktions-/Erledigt-Spaltenköpfe in API-Tokens, Feed-Tokens, Sitzungen und der Aufgabenübersicht (NodeView UND Server-Renderpfad) tragen visually-hidden-Beschriftungen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 2077d92c09 |
#166: Skip-Link, verstecktes Seiten-h1, Resizer in die Nav-Landmarke
Skip-Link als erster Tab-Stopp springt auf #main; die angemeldete Seitenansicht bekommt ein visually-hidden h1 (der sichtbare Titel ist ein Input, der jetzt auch ein aria-label trägt); der Sidebar-Resizer wandert in die nav-Landmarke (absolut an der Kante positioniert), damit kein Inhalt außerhalb von Landmarken liegt. Zwei e2e-Locator auf das Sidebar-Formular gescoped — das Editor-Titelfeld matcht seit dem neuen Label ebenfalls auf /title|titel/i. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 057992faaf |
#164: ARIA-Semantik — Editorfläche, Autocomplete-Listboxen, Sidebar, Toolbar
Die Editorfläche bekommt einen lokalisierten zugänglichen Namen und ist im Lesemodus role=document statt eines unbenannten Textfelds (setOptions im selben Layout-Effekt wie setEditable). Eingeklappte Sidebar zusätzlich inert (aria-hidden allein ließ fokussierbare Kinder im Tab-Weg). Die li-Zwischenknoten der Listboxen (Wikilink-/Mention-Autocomplete, Suchergebnisse) sind role=presentation, damit listbox→option wieder eine gültige Eltern-Kind-Beziehung ist. Toolbar: Pfeiltasten-Navigation über die Controls (native Selects behalten ihre Pfeiltasten) und ein sprechendes Toolbar-Label statt des Absatz-Buttons-Labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| f9d23bf8d0 |
#151: Mention-Benachrichtigungen über die Glocke
Neuer Notification-Typ mentioned; abgeleitete Tabelle page_mentions (Migration), vom Collab-Persist transaktional neu geschrieben — der Diff gegen den Vorzustand wird als pg_notify (page_mentions_changed) emittiert, nur NEU Erwähnte lösen aus (kein Spam bei Folge-Saves). Der api-Listener (erweitert um den zweiten Kanal) ruft NotificationsService.fanoutMentions: Zustellung nur nach canAccessPage-Recheck, die Autoren (pending contributors) benachrich- tigen sich nie selbst; Payload wie gehabt mit Actor-Namen. API-seitig erzeugte Seiten seeden page_mentions aus deriveContent. Glocken-Text de+en; DB-Test (Leser ja / Outsider nein / Autor nein); kompletter Loop live verifiziert (Tippen → Persist → NOTIFY → Notification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| e164370691 |
#154: Aufgabenübersicht als Kern-Block (Seite + Unterseiten)
Neuer Block-Atom task_overview (Markdown-Fence dorfteich-tasks, HTML-Placeholder). Shared extractTaskRows liest Task-Zeilen mit Text, Mentions (#150) und Start-/Zieldaten (#152); TasksService sammelt zur Lesezeit den Teilbaum (rekursiv via collectSubtreeIds, canAccessPage- Filter je Quellseite) aus Basis-State + page_updates-Log — KEINE abgeleitete Tabelle nötig (Teilbäume sind klein, kein Drift). Neuer auth-Endpoint GET /read/:pond/:slug/tasks; die öffentliche Ansicht expandiert den Placeholder serverseitig zur statischen Tabelle (Instanz-Sprache). NodeView mit Live-Tabelle und Rückschreib-Checkboxen (optimistisch, Override bis der debounced Collab-Persist nachzieht); Einfügen über die Block-Auswahl (eingebauter Eintrag). Unit- + DB-Tests, neuer CI-Pack tasks.spec (voller Loop inkl. Rückschreiben end-to-end), User-Guide-Doku en+de. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 92a3b2f6d5 |
#152: Datums-Marker >> (Zieldatum) / << (Startdatum)
Neuer Inline-Atom date_marker {kind: due|start, date: ISO}. Markdown
kanonisch ISO (>>2026-12-31), Eingabe-Kulanz dd.mm.yyyy; Block-Guard
vor blockquote hält zeilenführende >>Daten aus dem Zitat-Parser;
ungültige Kalenderdaten bleiben Text. Editor: InputRule beim Tippen
(+Leerzeichen), Anzeige per Intl.DateTimeFormat in Nutzersprache,
Überfällig-Färbung. Die User.locale-Verdrahtung existierte bereits
(auth-context, #17) — keine Änderung nötig. 6 Unit-Tests inkl.
Task-Listen-Zeile mit Marker und Mention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
|
|||
| 7471fc70f7 |
#150: @-Mentions — Inline-Node, instanzweite User-Suche, Autocomplete
Neuer Inline-Atom mention {userId, username}: Markdown-Regel @username
(E-Mail-sicher über Wortgrenzen), Serializer, HTML-Span dt-mention,
Plain-Text für die Suche, Extraktor extractMentionUserIds. Neue
Endpoints GET /users/search (auth, min. 2 Zeichen, Limit 10,
Rate-Limit) und GET /users/brief (Batch-Auflösung für live
Anzeigenamen; gelöschte Nutzer → toter Chip). Editor: MentionView mit
Live-displayName, MentionAutocomplete (Klon des Wikilink-Musters),
Chip-CSS. 5 Unit-Tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
|
|||
| 7252bd16e0 |
#149: Atom-Feeds für Teiche und Seiten, privat via Feed-Token
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m53s
CI / Build container images (pull_request) Successful in 4m1s
CI / Auth e2e pack (pull_request) Successful in 7m12s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m35s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Failing after 5m14s
CI / Import/export fidelity gate (push) Has been skipped
GET /public/:pond/feed.xml (zuletzt geänderte Seiten) und GET /public/:pond/:page/feed.xml (Versions-Historie), @Public mit 404-Semantik; öffentliche Teiche anonym, nicht-öffentliche über neues read-only Feed-Token je Nutzer als ?token=dt_feed_… (neue Tabelle feed_tokens + Migration, Verwaltung in den Nutzer-Einstellungen, FeedTokensSection). Öffentliche HTML-Seiten annoncieren den Teich-Feed per link rel=alternate. DB-Tests (anonym/privat/Token-Lifecycle) und User-Guide-Doku en+de. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| b4247f4832 |
#145: Einstellungsseiten mit Sektions-Sprungnavigation
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m27s
CI / Build container images (pull_request) Successful in 3m48s
CI / Auth e2e pack (pull_request) Successful in 6m58s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Failing after 4m47s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
Neue SettingsLayout-Komponente leitet die Navigation per MutationObserver aus den section>h2-Blöcken ab (erfasst konditionale und komponenten-eigene Sektionen ohne Verdrahtung), sticky Leiste neben dem Inhalt, auf schmalen Viewports horizontale Chip-Leiste; aktive Sektion über Scroll-Position, am Seitenende gewinnt die letzte. Auf allen vier Einstellungsseiten verdrahtet; die Admin-Grundeinstel- lungen bekommen dafür eine eigene Überschrift. Neuer CI-Pack settings-nav. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 9bd25f6ce3 |
#146: Transparente Einbettung $[[Seite]] ohne Rahmen und Titel
Neues bare-Attr am transclusion-Node; $-Präfix in Markdown-Regel, Serializer und Autocomplete; HTML-Placeholder trägt data-transclusion-bare, Server-Expansion und NodeView lassen bei bare Rahmen und Titel weg. Gleiche Tiefen-/Zyklen-/Permission-Regeln, zählt weiter als Link. Unit- und DB-Tests ergänzt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 15376d4ac2 |
#135 Seiten-Einbettung ![[Seite]] (Transklusion) im Lesemodus
Obsidian-Syntax `![[slug]]` (optional `![[slug|Anzeige]]`) als Seiten- Einbettung. Im Lese- und öffentlichen Modus wird der Inhalt der Zielseite inline gerendert; im Editier-Modus zeigt die NodeView eine Platzhalter- Karte (Titel + Öffnen-Link). Shared (Vorbild plugin_block): - Neuer Block-Atom-Node `transclusion` (targetSlug + optional displayText). - Markdown: Block-Regel für eine reine `![[…]]`-Zeile (vor `paragraph` registriert; mitten im Absatz greift sie bewusst nicht), Token→Node- Mapping, Serializer — Round-Trip stabil. - html.ts: Platzhalter `<div class="dt-transclusion" data-transclusion>`. - extractWikilinkSlugs erfasst jetzt auch Transklusionen → Einbettung zählt als Backlink/Graph-Kante. Backend (zentraler Render-Pfad): - PublicService expandiert Platzhalter zur gerenderten Body-HTML der Zielseite: SELBER Pond, read-permission-geprüft, Tiefe ≤2 + Zyklen- Guard (visited); Fehlend/unlesbar/zyklisch → neutraler Wikilink. Medien werden EINMAL über den ganzen Baum aufgelöst (kein Doppel-Processing). - Neuer authentifizierter Endpoint GET /read/:pondSlug/:pageSlug (nicht @Public) liefert dieselbe gerenderte HTML — für die NodeView im authentifizierten Lesemodus, auch bei nicht-öffentlichen Seiten. Web: - NodeView `transclusion.tsx`: Editier-Modus → Karte; Lesemodus → holt /read/:pond/:slug und rendert den (server-sanitisierten) Inhalt inline. - WikilinkAutocomplete unterstützt `![[` → fügt einen Transklusions-Block ein (statt Wikilink). - CSS für Karte (.dt-transclusion-card) und Embed (.dt-embed), i18n de+en. Tests: shared Round-Trip-Unit (5), public-DB-Test um Embed-Expansion (zyklus-sicher, Fehlend→Link) erweitert — grün. typecheck/lint/i18n grün. Visuelle Editor-Verifikation folgt auf dem Test-Stage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| c858f12592 |
#134 Statuszeile zwischen Navigation und Artikel
Neue schlanke Statuszeile (letzte Aktualisierung · Wortzahl · geschätzte Lesezeit) zwischen Seitenkopf und Artikel — im authentifizierten Lesemodus und in der öffentlichen Ansicht. - Geteilte Komponente `PageStatusBar` (Datum via Intl in der aktiven Sprache, Lesezeit = ceil(Wörter/200), Singular/Plural, Lesezeit ausgeblendet bei 0 Wörtern). - `countWords`/`htmlToText`-Helfer in lib/word-count.ts. - Authentifiziert (`PageEditorPage`, nur Lesemodus): Wortzahl aus dem vorhandenen Markdown-Export (geteilter Query-Key ['page-markdown']), `updatedAt` direkt von `page.data`. - Öffentlich (`PublicPageView`): Wortzahl aus dem server-gerenderten HTML per DOMParser — kein Editor-Bundle nötig; kein Backend-Change. - i18n common.statusbar (de+en), CSS `.page-statusbar` (middot-getrennt, gedämpft). Gates grün (typecheck/lint/i18n:check); visuell verifiziert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC |
|||
| 6c98a71d34 |
Favorites: personal page stars, golden icons, sidebar filter (#132)
Some checks failed
CD / Build and push images (push) Successful in 3m57s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Failing after 2m51s
CI / Import/export fidelity gate (push) Has been skipped
Semantics changed from the issue during planning (documented there, comment 1192): favorites are PERSONAL per user, not pond-wide — the sys-fav label approach is dropped entirely. Storage is a page_favorites table (userId+pageId, FK cascade); PUT/DELETE /pages/:id/favorite toggles idempotently and needs read access only (#60 404 semantics — a star is a note-to-self, not a page modification), GET /ponds/:id/favorites lists the account's stars sliced to still-readable pages. Trashed pages keep their rows, so restore keeps the star; purge cascades it away. Web: one shared ['favorites', pondId] query feeds the TopBar star (between labels and history, golden when set), the golden tree icons in the sidebar, and a latching "Favorites" filter button next to the view switch that narrows either view (combinable with the label filter). No public-API/MCP exposure — with the label approach gone, that parity is no longer free; favorites stay UI-only for now. New favorites e2e pack (star toggle, golden icon, filter, per-user isolation) wired into CI; DB suite covers the round-trip, read gating, and the trash/restore/purge lifecycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 48d4c60af7 |
Trash: checkbox multi-select with bulk restore and purge (#128)
Each trash row gets a checkbox, a toolbar above the list offers "select all" (native indeterminate for partial selections) and the two bulk actions; bulk purge confirms with the selection count (pluralized). Processing is sequential on purpose — purge promotes leftover children (#107), so concurrent tree mutations would race. Failures don't strand the rest: the loop keeps going, failed pages stay selected for a retry, and an alert banner reports the count. Single-row actions run through the same path, which also fixes their previously unhandled rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 36cdd4fbca |
Editor: confirm snapshots with a toast; wire ui.toast for plugins (#130)
Cmd/Ctrl+S used to snapshot silently. A new app-wide ToastProvider (components/Toast.tsx) owns a bottom-center stack — permanent polite live region, auto-dismiss after 2.5 s, click to dismiss early, error variant. Both snapshot paths (the keyboard chords in PageEditorPage and the save-version TopBar button) now confirm with the version name when there is one, and their failure alert becomes an error toast. The plugin host capability ui.toast (declared since #74, wired nowhere) connects to the same stack: PluginBlockScope carries the showToast handle, plugin-block passes it into the sandbox context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 0428892ef2 |
Editor shortcuts: "e" edits, the platform chord+S snapshots versions
In reading mode a plain "e" (guarded against typing targets) switches to edit mode. In edit mode the platform's native chord — Cmd on macOS, Ctrl elsewhere — +S saves an unnamed manual snapshot in place, and +Shift+S asks for a name and returns to reading mode; both always swallow the browser's save dialog. The shared isTypingTarget guard moves from TopBar into lib/keyboard.ts next to the new modifier helper. Unnamed snapshots needed the API to accept them: the version label is optional now (trigger stays MANUAL, label null), and the history list's existing null-label fallback text becomes "Manueller Schnappschuss" / "Manual snapshot" — it only ever shows for exactly those. DB test for the label-less path, e2e coverage in the CI content pack. Fixes #125 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 44a90a53ac |
Sidebar: pin the four actions as an icon row at the bottom
Graph, new page, import, and trash collapse from scattered text links into one icon row pinned to the sidebar's bottom edge, in that order, each with a hover hint (the trash reads "Papierkorb anzeigen"). The new-page button now toggles the inline form, which still renders above the footer with the same classes; the import trigger becomes an icon whose progress list floats above the row so the icons stay put. All e2e class hooks (.sidebar__new-page, .sidebar__graph-link, .sidebar__import-*) are unchanged. Fixes #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 1781f12f6e |
Knowledge graph: live Obsidian-like simulation with tunable physics
All checks were successful
CD / Build and push images (push) Successful in 4m2s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Successful in 4m28s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 6m42s
CI / Import/export fidelity gate (push) Successful in 49s
Release / Build release images and notes (push) Successful in 1m7s
Release / Release-candidate operations QA (push) Successful in 42s
Prod deploy / Deploy the released images to Prod (push) Successful in 21s
The force layout used to run once (tick(250)) and freeze; dragging moved a single node with no reaction from its neighbors. The simulation now stays alive: React renders the SVG structure (testids, edge/ring classes — the e2e contract is unchanged) while each tick writes positions imperatively into the element refs, and it settles to rest via alpha decay, which also keeps Playwright's stability wait happy. Dragging pins the node (fx/fy) and reheats the physics, so the neighborhood gets pulled along; a plain click still just opens the page. Surviving nodes keep their positions across data refreshes, e.g. when a phantom becomes a real page. The graph page gains four sliders — attraction, repulsion, node size, font size — persisted per pond (ui.graph.settings.<pondId>) with a reset; the inner view is keyed by pond id because usePersistentState reads its key only on mount (#108 trap). The local panel adopts those settings (no second set of sliders) and swaps the two fixed hop buttons for a 1–5 depth slider. Fixes #123 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| b8546eb249 |
Vault import: always show the label section, create labels inline
The label multiselect was gated on the pond already having labels — but before a first import that is the common case, so the section silently vanished and no import-wide label could be chosen. Render the fieldset unconditionally (with a hint when empty) and add an inline create field: POST the new label directly to get its id back, refresh the shared label query, and tick it right away. Same pond_admin permission as the dialog itself. The e2e pack now creates its label through the dialog instead of the API, covering exactly the empty-pond path that slipped through. Fixes #121 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 704ebe48a6 |
Vault import dialog in the pond settings (#118)
Some checks failed
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Deploy to Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Failing after 1m0s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Has been cancelled
An admin-only 'Import an Obsidian vault' section on the pond settings page opens a dialog with everything the #117 endpoint expects: the ZIP, an indented mount-parent picker over the page tree (the MovePageDialog pattern), a multi-select over the pond's label tree, and the frontmatter radio (strip / keep as code block). Submit uploads and polls the job with a vault-sized budget (600 x 1 s), then invalidates pages, graph, phantom-links, and labels so the sidebar tree, graph, and pickers show the import without a reload — and links to the mount page. apiUploadFile now takes extra multipart fields (the options JSON); existing callers are unchanged. e2e import-vault.spec.ts: an admin imports the fixture vault through the dialog and the app shows the folder tree under the mount page, a rewritten Obsidian link navigates to the right page, the embedded image renders, and the nested tag labels exist next to the dialog's extra label; a plain editor gets no section at all. 3x flake-free locally (CI wiring lands with #119). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 8ae010218e |
Obsidian vault import: endpoint, job orchestration, rollback (#117)
Some checks failed
CD / Build and push images (push) Successful in 4m5s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m21s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m22s
CD / Promote to Int (push) Successful in 11s
POST /ponds/:pondId/import/vault (pond-admin-gated; a vault import
creates a subtree, uploads files, and creates labels — administration,
not everyday editing) takes the ZIP plus a JSON options field
{parentPageId?, labelIds?, frontmatterMode}. The archive is parsed at
enqueue for fast 400s; the job (new kind import_vault, riding the
existing isImportKind worker routing) re-parses and runs the #116
transform, then: containers top-down → notes (asset placeholders →
uploaded pond files; non-images become page attachments) → tags to
labels (nested tags build a label hierarchy via LabelsService, so
locking and cache invalidation apply) plus the dialog labels.
All-or-nothing: any failure hard-deletes the created pages (children
first) and removes the stored files (quota restored), then surfaces as
import_vault_invalid_zip / import_vault_too_large / quota_exceeded /
conversion_failed — and makes the worker's retry policy safe.
Supporting changes:
- conversion_jobs gains a nullable options jsonb column; enqueue takes
kind-specific options and a maxInputBytes override (the 25 MiB
default protects the pandoc sidecar, which a vault never touches —
vaults use the 64 MiB upload limit).
- insertPage accepts a pre-reserved slug (the batch reserves all slugs
up front against pond ∪ batch).
- NEW: pages born with content seed their outgoing page_links rows
(deriveContent now returns wikilinkSlugs) — imported pages would
otherwise stay invisible to backlinks and the graph until their
first collab save. Collab still rewrites the rows on every save, and
the existing phantom resolution heals batch creation order.
import-vault.e2e.db.test.ts (4 tests, real worker drained): gating +
input rejection, the full fixture import (tree under a mount page,
collision suffixes, link rows incl. phantom, nested tag labels, extra
label everywhere, frontmatter stripped, image embedded + PDF attached),
complete quota rollback, and a clean re-import with fresh suffixes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|||
| 64e21e9f94 |
Offer creating the page on the not-found screen (#115)
Some checks failed
CD / Build and push images (push) Successful in 4m20s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m38s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Has been cancelled
Following a phantom wikilink now ends with a way out instead of a dead end: when the pond resolved and the page 404s as plain not_found, the error screen offers creating the page in place. Title = the URL slug (the PhantomPagesView mechanic), so every wikilink pointing at the address resolves; the invalidated page query then mounts the editor on the same URL. The affordance is deliberately ungated like the sidebar's new-page button — the client cannot tell 'never existed' from 'not readable' (#60), and a reader's POST surfaces as the regular 403 banner. The page_trashed branch (#31) is untouched. Rides along: PhantomPagesView now also invalidates ['pond-links'] — the graph views kept showing a just-created target as a phantom. e2e pack create-missing-page.spec.ts (CI wiring lands with #119): author a phantom link, follow it, create, backlink proves resolution; reader path asserts the 403 banner and no editor mount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| dbbe229cb9 |
Pond knowledge graph view (#112)
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
/p/:pondSlug/graph (static segment ranked above :pageSlug, same documented reserved-slug gap as trash/settings) renders the pond's readable wikilink graph from GET /ponds/:id/links: pages as nodes colored by their first label (legend included, DEFAULT_LABEL_COLOR for unlabeled), resolved links as edges, phantom targets as dashed nodes — clicking one offers to create the page, which resolves its links. Rendering is a self-contained SVG force graph: only d3-force is bundled (no d3 DOM/zoom modules, zero external requests); the layout runs synchronously to rest, zoom/pan/node-drag are plain pointer math. SVG over canvas deliberately — every node carries a data-testid the e2e packs can click. Ponds beyond 500 pages get a capped-view notice. Sidebar footer links every member to the graph (trash stays owner-only). New i18n namespace graph (de+en). Verified live: nodes/edges/legend render, node click opens the page, phantom click creates it and the node turns solid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 0308bc712d |
Drag-onto reparent, Move-to dialog, and the delete decision (#109)
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 3m58s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Successful in 4m23s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Has been cancelled
Sidebar folder view: a row now has three drop bands — the edges keep the within-group reorder, the middle band nests the dragged page under the row (appended to its new sibling group, with a drop-into outline cue). Cycle/depth refusals surface as a translated banner; successful moves are announced for screen readers. The overflow menu gains 'Move to…': a modal parent picker over the page tree (top level first, the page's own subtree disabled) that works in every sort mode. Delete now decides per case: childless pages keep the plain confirm; pages with subpages open a dialog offering promote (default wording: move subpages up) or subtree delete. The children lookup reads the CACHED pages list on purpose: an async fetch before window.confirm broke the click→confirm→DELETE rhythm the content pack (and users) rely on, and a stale childless read errs toward promote — never toward a silent subtree delete. Sidebar caret labels deliberately exclude the page title: accessible names are matched by substring in the specs (#101), and a title like 'Editor…' collided with the edit-mode toggle. Verified live: move dialog (subtree option disabled), promote and subtree delete flows; content/trash/export packs green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 15184876bd |
Sidebar folder view, label view, and the view toggle (#108)
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
The sidebar now presents pages as a collapsible tree built from parentId (folder view) or grouped under the hierarchical label tree (label view, read-only; multi-label pages appear under each label, untagged ones in an 'unlabeled' group). The pond owner sets the default via a new sidebarView pond setting (PATCH-merged like the other keys); every user can override it locally (ui.sidebar.view.<pondId>), and the toggle sits above the page list. Collapse state persists per pond. New pages created while a page is open become its children — the inline form says so and sends parentId. Reordering (buttons and drag-between) now operates within one sibling group; the label filter stays a folder-view feature and falls back to the flat list while active, so the filtered order is never mistaken for a partial tree. SidebarContent is keyed by pond id so the per-pond localStorage hooks mount with the right key. e2e hooks (.sidebar__pages, .sidebar__page, reorder buttons) kept; reorder/labels/content packs green locally, plus a live smoke of nesting, collapse persistence, and both views. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| eb6b0d5d02 |
Page hierarchy: parentId, create-under-parent, reparent (#106)
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 3m53s
CD / Deploy to Test (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 4m16s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m19s
CD / Promote to Int (push) Successful in 14s
CI / Auth e2e pack (push) Has been cancelled
Pages form a tree via a nullable parent_id self-relation (SetNull backstop; the real trash/purge semantics follow with #107). Slugs and URLs stay flat and pond-unique, so moving a page never breaks links. - Shared: generic parent-id tree helpers in tree.ts (labels re-export them; buildLabelTree keeps its name-sorted behavior), MAX_PAGE_DEPTH=6, parentId on PageView, createPageInputSchema.parentId (nullish), repositionPageInputSchema.parentId (optional; absent = keep parent). - API: create validates the parent (same pond, live, depth); PATCH /pages/:id/position reparents atomically with the placement, rejecting cycles (page_cycle) and depth violations (page_depth_exceeded); GET /ponds/:id/pages nulls parentId when the caller may not read the parent, so hidden page ids never leak. - New error codes translated de+en; hierarchy.db.test.ts covers create, 404s, depth, cycle, atomic reparent, and the permission nulling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| abf49c7c0e |
Give the landing-page save button its own label
Some checks failed
CD / Build and push images (push) Successful in 3m53s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m12s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Failing after 2m56s
CI / Import/export fidelity gate (push) Has been skipped
The landing form reused the legal namespace's "Save legal pages" label, so two buttons shared that text and the legal e2e's page-wide button lookup hit a strict-mode violation. Use a dedicated settings-namespace "Save landing page" label instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |