Make the wiki UX reproducible: set_navigation writes the custom static sidebar
(Home + Anwender + Betrieb, betrieb items visible only to admin groups 1/3);
ensure_html_rendering turns on markdownCore.allowHTML (v-encoded, other renderer
configs preserved) so the HTML card landing renders — note it needs a Wiki.js
restart to take effect on a fresh instance. The injectCSS is rebuilt: content links
get an underline + accent (the old broad primary--text coloring made non-links look
like links) plus the landing card/button styles. Page content lives in git-storage.
Downscale alpenglow.jpg 2560->1920px (604KB -> 400KB, visually identical on the
login screen). With the smaller background everything fits in one platform-branding
ConfigMap again (~752KB / 1MiB), so drop platform-branding-bg and its /_assets/img/bg
mount; authLoginBgUrl goes back to /_assets/img/branding/alpenglow.jpg. Favicons kept
as ThreadNet.
The browser tab still showed Wiki.js because only favicon.ico + 16/32 were
replaced — the larger icons the tab picks (android-chrome-192, apple-touch-180,
mstile-150) were still the defaults. Add ThreadNet versions and subPath-mount
them. Adding these pushed the single branding ConfigMap to ~1.04 MB (the 1 MiB
etcd limit), so split the 604 KB background into its own platform-branding-bg
ConfigMap mounted at /_assets/img/bg; authLoginBgUrl follows.
Add wikijs-backup, a nightly Borg CronJob (03:30) that dumps the Wiki.js postgres
DB to the Storage Box — the content lives in git-storage, but comments, local
accounts and the search index do not, so the runtime state gets its own backup.
Mirrors authentik-backup (DB-only), reusing synapse-backup-credentials and
-known-hosts with a separate repo path; NetworkPolicy lets the backup pod reach
wikijs-postgres. Also flip tocPosition to right so the page TOC sits beside the
content instead of in the left rail.
The Betrieb/Anwender content structure is created (lives in git-storage, not the
job). Add a page rule so wiki-anwender can read the home page besides /anwender —
otherwise "/" would be denied for them. betrieb/* still matches no rule, so
Wiki.js' default-deny keeps it invisible to users (verified in checkAccess:
returns match && !deny). rule() gains a match param for the EXACT home rule.
Add canonize_wiki to the daily schedule: bare-clone the public Gitea wiki repo
(sorb/ThreadNetWiki) and push its main to git.lab axion1337.chat/threadnet-wiki,
closing the second half of ADR-0015 (Wiki.js->Gitea->canonize->git.lab, the same
reversal as the TURN rotation). The target was created empty, so the first push
creates main and later runs fast-forward — no force, branch protection intact.
Auth via the WIKI_CANONIZE_TOKEN CI variable (project token, write_repository).
Set the blue Docusaurus-style accent (#2b6cb0 light / #63b3ed dark) via injectCSS
on the app UI — the login page is excluded because Wiki.js renders it without
custom CSS, so a dark login card is not themeable and is left as-is. Mount the
ThreadNet favicon (favicon.ico + favicon-16/32 PNGs the browser tab uses) from the
platform-branding ConfigMap via subPath, overlaying only those files.
The site title (top-left + browser tab) was the hardcoded "Wiki.js". Drive it
from a deployment variable instead; the config job sets site.title from
WIKI_SITE_TITLE (default "Wiki"), set here to wiki.axion1337.chat.
Wiki.js cannot reach git.lab from the cluster, so its content syncs to Gitea
(sorb/ThreadNetWiki) over HTTPS with a dedicated deploy PAT; a CI job canonizes
Gitea->git.lab (TURN-rotation pattern). The config job's ensure_git_storage
configures the git storage target (mode sync, config values {v:...}-encoded like
the auth strategy). Repo/user/branch/author are plain env; only the PAT lives in
the SOPS secret wikijs-git-secret, wired optional so the job still runs without it.
Flux's default source ignore excludes *.png/*.jpg, so the platform-branding
configMapGenerator failed with "no such file or directory" for the branding
assets. Re-include just those two files via .sourceignore.
Serve the ThreadNet logo and the shared platform login background (alpenglow.jpg,
same file Authentik and Element use) as public static files under
/_assets/img/branding, mounted from a single platform-branding ConfigMap. This
avoids two bad patterns: linking the background via an external URL (runtime
dependency on axion1337.chat) and uploading the logo as a gated Wiki.js asset
(which 404/403s on the unauthenticated login page unless guests get read:assets).
Wiki.js serves /wiki/assets publicly at /_assets, so mounted files need no
read:assets — guests stay locked out of pages. The config job sets logoUrl and
authLoginBgUrl to the local paths and enables dark mode as default. The ConfigMap
uses a name hash so a branding change rolls the pod. It can later be mounted into
Authentik/Element too, keeping one source of truth for the shared assets.
Disabling local (previous approach) left the login page empty: Wiki.js needs a
form strategy to render the provider list, so with only OIDC the card was blank
and login hung. Keep local ENABLED and instead set the built-in authHideLocal
site config, which removes local from the provider list on the login page —
only "Login with Authentik" shows.
local stays usable because the config job logs in through it, and it is the
break-glass path, reachable via /login?all (Wiki.js honors ?all to bypass
hideLocal). Verified live end-to-end: job runs idempotently, both strategies
enabled, authHideLocal=true.
Nobody should log in manually. The job disables the local strategy as its final
step (with a still-valid JWT), so the login page offers only Authentik OIDC.
Re-runs without a DB reset find local disabled -> login returns None -> the job
exits cleanly (already configured). Break-glass = DB reset (finalize re-enables
local). Verified live: local login is BLOCKED after the run.
The OIDC strategy failed at startup ('requires an issuer option') because the
config values never persisted: Wiki.js reads each via _.get(JSON.parse(value),
'v', null) (source-verified in server/graph/resolvers/authentication.js), so the
value MUST be {"v": <value>}. None of the earlier encodings had the 'v' key.
Fixed and verified live: strategy now loads [ OK ].
Idempotent GraphQL bootstrap job (verified live): /finalize with the random SOPS
admin, then the OIDC strategy and the groups 'authentik Admins' (full) and
'wiki-anwender' (read /anwender), Guests locked. Replaces the manual setup wizard
entirely. OIDC client_id/secret in a SOPS secret; NetworkPolicy lets the job reach
wikijs. Script as a fixed-name ConfigMap; re-run = delete the Job.
Random Wiki.js admin credentials (SOPS, never displayed) for the config Job to
bootstrap setup non-interactively. Authentik group wiki-anwender (curated readers
of /anwender); admin role = existing authentik Admins. Part of making Wiki.js
fully deployable - no manual setup wizard.
Deployable group structure for the wiki roles/abschottung. Names match the
Wiki.js groups and flow to Wiki.js via the profile->groups claim. Members are
assigned in the Authentik UI (sorb), not here.
Replace the PROVIDERKEY placeholder with the actual Wiki.js OIDC strategy callback
(login/<strategy-key>/callback) so Authentik accepts the redirect back. Closes the
login loop for wiki.axion1337.chat.
The wiki-oidc-provider blueprint referenced !Env AUTHENTIK_WIKI_OIDC_CLIENT_ID/
SECRET which were never set, so the blueprint failed (status: error) and no
provider was created. client_id isn't a secret and the client_secret is read
into Wiki.js manually anyway, so drop both !Env refs and let Authentik generate
them. Read the values from the Authentik UI into Wiki.js. Guide 10 design fix.
The SOPS-encrypted secret existed locally but was untracked and not in the
kustomization, so Flux never applied it and both wiki pods stayed in
CreateContainerConfigError (secret not found). Add it to resources and commit.
Values not read; age recipient matches .sops.yaml so Flux can decrypt.
Wiki.js + a dedicated Postgres + public ingress for wiki.axion1337.chat, added to
the production kustomization so Flux applies it. Both pods intentionally wait on
the SOPS secret `wikijs-postgres-secret` (username/password) until sorb creates it
— same loud-but-visible pattern as the concierge bot. NetworkPolicy: Traefik ->
wikijs:http, wikijs -> wikijs-postgres:postgres (ingress-only, named container
ports). Ingress/Cert mirror the authentik pattern (letsencrypt-prod). Native OIDC
login (#0049, guide 10) and git-storage are configured post-start. All manifests
validated server-side (kubectl --dry-run=server).
How an Anwender/Admin reaches the suite wiki: public wiki.axion1337.chat, same URL
for both, Wiki.js's own OIDC login to Authentik (no forward-auth/outpost - that was
only for static Docusaurus). Ready-to-apply Ingress+Certificate manifest (authentik
pattern), the Authentik OAuth2 provider blueprint (parallel-safe slug wiki-js), and
the Wiki.js OIDC endpoints. Prepared, not deployed; supersedes guide 09 for the
suite (ADR-0014). Indexed in the guides README.
The 'Regelwerk Bindung resultiert in False' denial after login means the user
is not a member of wiki-zugang (step 2.4). Record it plus the redirect-loop and
outpost-offline cases so the next person recognises them fast.
A user hit the 'Neue Outpost-Integration' dialog, which only offers Docker/K8s
and can't be left empty. That is the service-connection creator, not the outpost.
Spell out: go to Applications -> Outposts -> Create (not Outpost Integrations),
and leave the Integration dropdown on 'No integration'.
Records the #0024 decision (axionwiki.lab) and flags it as a development-time
arrangement: the wiki still has to move into the ThreadNet Server Suite, and
surface alternatives beyond BookStack get re-examined afterwards. Expands the
Authentik outpost/token steps (version pinning, exact UI path, where the token
goes) and the Overmind/Traefik side (shared network, redirect-loop caveat, full
authResponseHeaders, request walk-through).
Docusaurus is static and has no auth of its own, so the gate goes in Traefik in
front of it via an Authentik proxy outpost. Guide 09 carries the complete,
ready-to-apply config: the Authentik blueprint (forward_single proxy provider +
application + wiki-zugang group + policy binding) and the Overmind side (proxy
outpost container + Traefik forwardAuth middleware). Deliberately kept as a guide,
not merged into the applied blueprint ConfigMap: nothing goes live until the
wiki hostname (#0024) is settled and the outpost token (a credential) is created
by sorb. Dedicated outpost on Overmind so per-request checks stay local and only
the login redirect crosses the VPN. Indexed in the guides README.
@apo could log in and message but no call would start — the click produced
zero server activity. Root cause: no Synapse profiles row, which makes every
displayname write 500 in _check_profile_size (NoneType), leaves the account
without a display name, and prevents the Element Call widget iframe from
initialising. Documents diagnosis (open_id_tokens=0 is the tell), the
cross-checked INSERT fix, and who is affected. Indexed in the troubleshooting
README.
The Authentik upstream provider used claims_imports.localpart.on_conflict:
add, which links a newly registered upstream identity to an EXISTING local
account when the derived localpart collides. Combined with Authentik's
case-sensitive username uniqueness, any holder of an invitation token could
register a case variant of an existing name and be linked into that account,
including service accounts (draupnir, alerts, maintenance-notify) that have
no upstream link at all.
Set on_conflict: fail so a colliding localpart aborts provisioning instead.
Existing links are unaffected.
Wrote 08-concierge-gaeste-einladungen.md without adding it to the guide index - the same class of oversight this whole session has been about. Fixed within the hour instead of finding it later by accident.
Said 7 themes, listed Gruvbox Dark and Wal by name - neither exists in element-values.yaml. Actual count verified against the live config: 17. Pointing at management/shared/branding.md as the single place that lists them with colours and light/dark labels instead of duplicating the list here, which is what let this drift in the first place.
Turns guest onboarding from an admin-only click in the Authentik UI into a traceable command a defined circle can run: !einladen creates a single-use invitation valid for three days, !verlaengern extends it twice at most, !freischalten makes it permanent, and expired accounts are deactivated automatically.
Authorisation is deliberately twofold - the Authentik group decides, the invite room makes it visible. A group alone leaves no trace of who invited whom; a room alone would authorise anyone who gets in.
Two deployment details matter: exactly one replica with Recreate, because a second instance would execute every command twice; and the script ConfigMap keeps its name hash so a change actually restarts the pod, avoiding the trap described in #50.
Endpoints and field names were taken from the running Authentik OpenAPI schema, not guessed. Refs axion1337.chat/axion1337.chat-gitops#48
Without bug_report_endpoint_url the whole section in Settings, Help and About is hidden, so a user has no way to obtain their logs at all. The element.io endpoint was deliberately removed on 2026-08-01 to stop logs flowing there.
The special value local brings the capability back without the outflow: the button reads Download logs, the dialog writes them to the device, and nothing is uploaded anywhere. Users can then attach them in Matrix, which is the path that is already in use.
Config only - no rebuild. Refs axion1337.chat/ThreadNet-Web#9
The anonymisation rewrite of 2026-08-07 gave every touched commit a new SHA, leaving the references in these documents pointing at objects that no longer exist. The mapping was reconstructed from the backup branches and each pair verified by tree and commit message before substituting.
Prefix lookups were built for lengths 7 to 12 and any ambiguous prefix would have been skipped; none were ambiguous across all 251 pairs.
The actual root cause of the failing blueprint, visible only after the KeyOf references were gone: the entry set recovery_flow alone, but the serializer validates the whole object and rejected it with 'When no user fields are selected, at least one source must be selected'.
All other fields are listed on purpose even though they match the model defaults - whatever the serializer does not receive is reset to its default, so a blueprint naming a single field can silently flip unrelated settings. Values are taken from the live database.
Refs axion1337.chat/axion1337.chat-gitops#60
Adding name and title was necessary but not sufficient - the blueprint kept failing. Running ak apply_blueprint by hand showed why the real error was never visible: when authentik logs a failed entry it calls str() on the KeyOf object, whose __repr__ resolves against an EMPTY blueprint and raises the very same exception. The logging path masks the actual validation error.
KeyOf also couples every stage binding to that one entry succeeding in the same run. Find resolves against the database instead, which is what the neighbouring blueprints in this same ConfigMap already do, and the flow exists.
Refs axion1337.chat/axion1337.chat-gitops#60
The flow entry only set designation. FlowSerializer requires name and title as well (neither has a model default), so validation failed, the entry never got a model instance, and every following KeyOf matrix_recovery_flow reference failed with it. The blueprint had status error on every run.
Corroborated inside the same ConfigMap: matrix-invitation-flow sets name and title and applies successfully.
Values are taken from the live database so nothing changes on the page users see. The typo in mail-adress is left as is - changing user-facing text would be a separate decision.
Closesaxion1337.chat/axion1337.chat-gitops#60
Dazu eine Aussage korrigiert, die seit heute nur noch halb stimmt: "2FA-Selbsteinrichtung optional (not_configured_action=skip)" gilt weiterhin fuer Mitglieder, aber nicht mehr fuer Admins.
Festgehalten sind vor allem die beiden Fallen: configure statt deny (deny sperrt Admins aus, ohne Weg zurueck ausser ueber den Cluster) und last_auth_threshold gegen die doppelte Abfrage. Dazu die wichtigste - stimmt der Gruppenname nicht, greift die Regel fuer niemanden und wirft dabei keinen Fehler.
Widget-Abhaengigkeit auf 0.19.2-threadnet.7. Umgesetzt ueber VITE_PRODUCT_NAME, ohne eine einzige geaenderte Quelldatei im Fork - upstream sieht die Variable an jeder Stelle vor.
Schliesst den dritten Punkt aus #54 ab.
Eigene Validate-Stage hinter der vorhandenen, gebunden an die Admin-Gruppe. Fasst KEIN Authentik-Standardobjekt an: not_configured_action haengt an der Stage statt an der Bindung, eine Umstellung am Standard haette alle Mitglieder getroffen.
not_configured_action=configure statt deny - wer keinen Faktor hat, wird beim Login durch die Einrichtung gefuehrt statt ausgesperrt. Bei einem Admin-Konto gaebe es nach einem deny keinen Weg zurueck ausser ueber den Cluster.
last_auth_threshold=hours=1 verhindert die doppelte Abfrage: die Standard-Stage auf Ordnung 30 validiert bereits, wer einen Faktor hat; diese hier ueberspringt sich dann.
BEWUSST auf einem Branch: die Gruppe ist mit dem Authentik-Standard "authentik Admins" angenommen und von sorb noch nicht bestaetigt. Trifft sie nicht zu, greift die Regel fuer niemanden - das waere schlimmer als keine Regel, weil es sich sicher anfuehlt.
Wichtigster Punkt: solange branding_logo hier steht, gewinnt der Blueprint. Wer das Logo in Authentiks Oberflaeche auswaehlt, sieht es bis zur naechsten Reconciliation - danach ist es wieder weg. Ein neues Logo gehoert in diese Zeile.
Dazu die Liste der oeffentlich erreichbaren Assets und der Hinweis, dass die vorhandene Wortmarke im wiki-Repo liegt und von Hetzner aus nicht erreichbar ist - sie muesste erst mit dem Client ausgeliefert werden.
Unser 512-px-PNG rendert in der Anmeldemaske viel zu gross: Authentiks Default ist ein SVG, das sich der Box anpasst, ein PNG dagegen nimmt seine Naturgroesse. sorb entwirft ein passendes Logo.
Wichtig: explizit auf den Default gesetzt, nicht die Zeile geloescht - ein Blueprint mit state: present setzt weggelassene Felder nicht zurueck, der alte Wert bliebe in der DB stehen. Titel, Favicon und Hintergrund bleiben.
Elements lake.jpg weicht dem Alpengluehen (John Towner, Unsplash License), Danksagung entsprechend umgeschrieben. Image rohana.axion1337.de/sorb/threadnet-web:v0.4.2 aus Pipeline 192.
Voraussetzung fuer den naechsten Schritt in gitops#54: die ThreadNet-Brand in Authentik referenziert genau diese Bilddatei und liefe vorher ins Leere.
branding_title/logo/favicon/default_flow_background auf dem bestehenden Brand-Eintrag (domain authentik-default) - kein zweiter Brand, sondern derselbe, um den die default_application schon haengt.
Die Assets werden von der Client-Auslieferung referenziert statt in Authentik hochgeladen: sie liegen dort ohnehin, und eine zweite Kopie ist genau die Quelle, aus der spaeter Abweichungen entstehen. Ein ConfigMap-Mount scheidet aus - die drei Dateien reissen base64-kodiert das 1-MiB-Limit.
BEWUSST auf einem Branch: alpenglow.jpg existiert erst nach dem naechsten Client-Deploy. Auf main gemergt wuerde Flux das sofort ausrollen und die Anmeldeseite haette einen 404 als Hintergrund. Reihenfolge: erst Client-Image, dann dieser Merge.
Ich hatte registration/passwordReset/deactivate auf false gesetzt mit der
Begruendung, der Web-Client zeige Schaltflaechen, die ins Leere fuehren.
Gemessen hatte ich nur zweierlei: dass UIFeature in der Config fehlt und dass
der Homeserver Registrierung ablehnt. Dass daraus sichtbare Schaltflaechen
folgen, habe ich NICHT geprueft - sorb hat auf /#/login nachgesehen, dort gibt
es keinen Registrieren-Button. Element blendet ihn im SSO-Flow ohnehin aus.
Die Aenderung loeste damit ueberwiegend ein Problem, das es nicht gab - und
deactivate: false nahm zusaetzlich die Konto-Loeschung aus den Einstellungen,
die vorher sichtbar war. Das hatte niemand verlangt.
Zurueck auf den Stand davor. Sollen einzelne dieser Schalter bewusst gesetzt
werden, ist das eine eigene Entscheidung mit eigener Begruendung.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der Web-Client zeigte Registrierung, Passwort-Reset und Konto-Loeschung an - alle
drei funktionieren auf dieser Instanz nicht:
- Registrierung ist am Homeserver abgeschaltet (gemessen: M_FORBIDDEN,
'Registration has been disabled. Only m.login.application_service
registrations are allowed.')
- Login laeuft ausschliesslich ueber SSO (Flows: m.login.sso, m.login.token) -
Passwoerter liegen in Authentik, nicht im Homeserver
- Konto-Loeschung laeuft ebenfalls ueber die Identitaetsverwaltung
Der Desktop-Client blendet die drei seit jeher aus; nur die Web-Config hatte
UIFeature gar nicht gesetzt. Damit sahen Web-Nutzer Angebote, die entweder in
einer Fehlermeldung enden oder am eigentlichen Weg vorbeifuehren - und
Registrierung widerspricht ausserdem der Vision-Entscheidung 'kontrolliert
wachsend, jeder Eintritt wird freigegeben'.
Chirurgisch eingefuegt, 17 Themes unberuehrt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Nachtrag - der Bump war im vorigen Commit an einer falschen Einrueckung im
Suchtext gescheitert (8 statt 10 Zeichen, derselbe Fehler wie beim v0.4.0-Bump).
Diesmal zeilenbasiert statt ueber einen mehrzeiligen Suchtext.
Bringt: Tab-Titel ThreadNet, favicon.ico, Fehlerseite und Desktop-Hinweis mit
eigener Marke.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Hebt Prod auf v0.4.1 (Tab-Titel, favicon.ico, Fehlerseite, Desktop-Hinweis).
Dazu logo_link_url: Elements Default zeigt auf element.io - ein Klick aufs Logo
haette also von unserer Anmeldemaske weggefuehrt. Zeigt jetzt auf die eigene
Instanz. Reine Config, wirkt sofort.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Auf der Anmeldemaske stand weiterhin Elements gruenes Logo - meine Icon-Arbeit
deckte nur vector-icons/ ab, also Favicon, PWA und App-Icon. Das Logo IM Client
kommt aus einer anderen Quelle: AuthHeaderLogo.tsx und HomePage.tsx lesen
branding.auth_header_logo_url aus der Config und fallen sonst auf
themes/element/img/logos/element-logo.svg zurueck.
Das ist reine Konfiguration - kein Rebuild noetig, die ConfigMap reicht.
Als Ziel bewusst vector-icons/512.png statt einer neuen Datei: Die liegt bereits
ausgeliefert (verifiziert, HTTP 200) und stammt aus derselben Quelle wie alle
anderen Icons. Eine zweite Logo-Datei koennte davon abdriften - genau die Falle,
die in shared/branding.md steht.
Randbedingung, die die Wahl einschraenkt: webpack kopiert aus res/ nur themes/**
und vector-icons/** nach webapp. Ein Logo unter res/img/ waere gar nicht per URL
abrufbar.
Chirurgisch eingefuegt, YAML und eingebettetes JSON validiert, 17 Themes
unveraendert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Hebt das Image von v0.3.0 auf v0.4.0. Damit sind die zentrierten Icons, die
Markenfarbe #ed4f4c in der Browser-/PWA-Leiste und die About-Attribution
'ThreadNet — powered by Element' unter der Client-Version live.
Der Weg ist der in der ThreadNet-Web-CI dokumentierte: Tag pushen -> docker_web
baut und pusht rohana.axion1337.de/sorb/threadnet-web:v0.4.0 -> Tag-Bump hier
deployt es. docker_web lief in Pipeline 176 gruen (101 s).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Die Ausnahme 'deploy-handover issues stay on the Gitea tracker' ist am
2026-08-02 entfallen - die Issues sind nach git.lab umgezogen, der Gitea-Tracker
ist leer. Damit stimmt jetzt auch der Satz weiter oben, dass kein Repo mehr auf
Gitea bearbeitet wird: Issues leben ohne Ausnahme auf git.lab, geschrieben wird
dort nur noch von der TURN-Rotation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Schedule ist angelegt und zweimal probegelaufen; offen bleibt nur das
CANONIZE_TOKEN, jetzt mit Issue-Verweis (gitops#51) statt einer allgemeinen
Anleitung. Auch die beiden Probelaeufe sind vermerkt - Pipeline 159 hat die
fehlende Lab-CA aufgedeckt, 161 lief gruen durch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der Probelauf des neuen Jobs ist an genau der Stelle gescheitert, an der man es
nicht vermutet: 'git fetch gitea' lief durch (Gitea ist oeffentlich lesbar),
'git fetch origin main' dagegen brach ab mit 'unable to get local issuer
certificate'. Der Runner stellt die Lab-CA nur seinem eigenen Klon-Schritt
bereit, nicht dem Job-Script - der Klon oben im Log gelingt, der eigene
git-Aufruf zwei Zeilen spaeter nicht.
Zwei Aenderungen:
1. Die CA-Kette liegt jetzt als ci/lab-ca-chain.crt im Repo und wird per
GIT_SSL_CAINFO gesetzt (CI_SERVER_TLS_CA_FILE zuerst, falls ein spaeterer
Runner sie doch liefert). Ein CA-Zertifikat ist oeffentlich, kein Geheimnis -
dieselbe Loesung wie im wiki-bookstack-Repo. Verifiziert: die Kette
(aXionLabs Root + Intermediate) validiert https://git.lab.
2. 'git fetch origin main' faellt ganz weg. Der Runner hat main mit voller
Historie schon ausgecheckt, CI_COMMIT_SHA ist dessen Stand - ein Netzaufruf
weniger und eine Fehlerquelle weniger.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Vorher brach der Job ohne CANONIZE_TOKEN sofort ab - auch dann, wenn gar keine
Rotation offen war. Das haette taeglich eine rote Pipeline erzeugt, bis das
Token existiert, und ausgerechnet die Gitea-Strecke ungetestet gelassen.
Jetzt laeuft der Job erst bis zur Feststellung 'nichts zu tun' durch (verifiziert
dabei Fetch und Erkennung) und verlangt das Token nur, wenn wirklich gepusht
werden muss. Rot wird die Pipeline dann, wenn es zaehlt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Drei Dinge, die zusammengehoeren.
1. Falscher Satz raus. 'there is no direct-to-Gitea exception left' stand seit
eff643e (2026-08-02, von mir) achtzehn Zeilen ueber einem Absatz, der eine
laufende Ausnahme beschreibt - der Wiki-Umzug hatte die letzte Ausnahme auf
REPO-Ebene beseitigt, ich hatte das zu 'gar keine mehr' verallgemeinert.
2. Das Warum der Gitea-Kopie ergaenzt. Bisher stand nur der Mechanismus da
('the cluster pulls from Gitea'), nicht der Grund: git.lab haelt die
Bauplaene, Gitea eine Kopie, die der Cluster OHNE verfuegbares Lab erreicht.
Ohne diese Begruendung sieht der Aufbau nach Altbestand aus - eine spaetere
Session koennte die Flux-Quelle auf git.lab 'geradeziehen' und genau die
Lab-Unabhaengigkeit zerstoeren, fuer die sie da ist. Steht jetzt als
ausdrueckliche Warnung in beiden CLAUDE.md.
3. Den monatlichen Handgriff abgeschafft. Der Rotations-CronJob laeuft im
Cluster, erreicht git.lab nicht und pusht nach Gitea; von dort musste die
Rotation bisher per Hand ueber git.lab zurueck. Wird das vergessen,
ueberschreibt der naechste Mirror-Push sie und Flux spielt still das ALTE
Shared Secret wieder ein - ein Fehler ohne Symptom.
Der Schedule-Job canonize_rotation holt jetzt taeglich jeden
turn-secret-rotation-*-Branch von Gitea, der nicht in main steckt, merged
und pusht ueber git.lab. Taeglich statt monatlich zum Rotationstermin, weil
ein monatlicher Lauf genau einen Versuch haette.
Faellt etwas aus dem Rahmen - Merge-Konflikt oder ein Secret ohne ENC[ -,
bricht der Job ab und pusht NICHTS. Die rote Pipeline ist der Alarm; ein
zusaetzlicher Termin waere wieder ein Todo, das man vergessen kann.
Verifiziert: YAML parst, alle elf Script-Bloecke sind gueltige sh-Syntax, und
die Kernlogik gegen den echten Repo-Stand durchgespielt - beide vorhandenen
Rotations-Branches werden korrekt als 'schon in main' uebersprungen.
Noch offen (braucht Rechte, siehe Dateikopf): Project Access Token als
CANONIZE_TOKEN hinterlegen und den taeglichen Schedule anlegen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
'siehe Backlogs CFGMON-11' - das Repo heisst seit dem PM-Framework-Umbau
management (ADR-0005). Zu CFGMON-11 gibt es kein Issue, die Arbeit war beim
Umzug abgeschlossen; deshalb datiert statt verlinkt. Reiner Kommentar, die
Job-Definition ist unberuehrt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Die Datei ist der Ort, an dem jemand Themes aendert - und der Ort, an dem nicht
stand, woher die Farben kommen. Genau so entstand die erfundene Fassung. Drei
Zeilen Kommentar mit Quelle, Verweis auf shared/branding.md und Hinweis auf die
zweite Kopie in ThreadNet-Web.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Grundlage sind jetzt die Definitionen aus Anthropics theme-factory-Skill
(github.com/anthropics/skills/skills/theme-factory) statt meiner Interpretation
der Namen. Meine erste Fassung traf bei fast allen daneben - am deutlichsten bei
Sunset Boulevard, wo ich kraeftiges Koralle/Pink baute statt der vorgegebenen
Terrakotta-Palette #e76f51/#f4a261/#e9c46a/#264653.
Ob ein Theme hell oder dunkel gemeint ist, steht in den Beschreibungen teils
widerspruechlich ('Warm Sand - backgrounds' bei einem Theme, dessen Showcase-Seite
dunkel ist). Deshalb aus theme-showcase.pdf gemessen: sieben der zehn sind hell,
nur Sunset Boulevard, Golden Hour und Desert Rose dunkel. Vorher hatte ich sechs
faelschlich als dunkel angelegt.
Ableitung je Theme: die vier Originalfarben als Akzent/Sekundaer/Highlight/Text,
Flaechenabstufungen daraus gemischt, Username-Farben als Mischungen derselben
Palette - damit bleibt jedes Theme in sich stimmig.
Chirurgisch: nur die colors-Bloecke und is_dark der zehn Themes (299 Zeilen gegen
299), YAML validiert, die uebrigen sieben Themes unberuehrt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Meine erste Auslegung des Namens waren kraeftige Sonnenuntergangstoene (Koralle,
Pink, Weinrot) - gemeint war die Hollywood-Hills-Richtung. sorbs eigene Wahl in
der BookStack-Oberflaeche macht das eindeutig: fuenf seiner sieben Farbwerte sind
der Coolors-Satz #264653/#2A9D8F/#E9C46A/#F4A261/#E76F51.
Genau der liegt jetzt auch dem Element-Theme zugrunde: Charcoal als Grundton,
Burnt Sienna als Akzent, Sandy Brown und Saffron als Sekundaerfarben, Persian
Green fuer Auswahl und Hervorhebung, warmes Beige als Text.
Chirurgisch: nur der colors-Block dieses einen Themes ersetzt (31 Zeilen gegen
31), YAML validiert, die uebrigen 16 Themes unberuehrt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
aXion1337 Light ist das exakte helle Gegenstueck zum Lieblingstheme: 'aXion1337
Dark' ist Gruvbox Dark (Farben und die acht Username-Farben sind die
Gruvbox-Palette), also ist Gruvbox Light das stilistische Analogon - gleiche
Rollenverteilung, gleiche Akzentfamilie, nur die Helligkeitsachse gespiegelt.
Dazu zehn benannte Paletten mit vollem Schluesselsatz (24 Farben inkl.
username-colors): Ocean Depths, Sunset Boulevard, Forest Canopy, Modern
Minimalist, Golden Hour, Arctic Frost, Desert Rose, Tech Innovation, Botanical
Garden, Midnight Galaxy - vier davon hell.
Chirurgisch eingefuegt (Lehre vom 2026-07-30): die YAML wurde nicht geparst und
nicht neu serialisiert, nur Text vor der schliessenden Array-Klammer ergaenzt.
Diff-Bilanz 0 entfernte / 427 neue Zeilen, YAML und eingebettetes JSON validiert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der naechtliche YAML-Redump (9aa42f9/e963203) hatte Kommentare und
Formatierung der Datei zerstoert - semantisch war er korrekt (verifiziert),
aber wartungsfeindlich. Jetzt: Original von c631b0a + exakt die 5 Zeilen
aus ThreadNet-Web#1 (4 Feature-Flags + setting_default). Effektive Config
byte-identisch mit dem bereits deployten Stand.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der TURN-Rotations-CronJob erreicht git.lab nicht und oeffnet seinen PR
weiter auf Gitea - dokumentiert, wie er kanonisch zu mergen ist
(Beispiel 640c934 vom 2026-08-01), inkl. Mirror-Force-Sync-API.
Karpathy-Guidelines unveraendert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Web-Deploy war Teilmenge der Desktop-Defaults: video_rooms, group_calls,
element_call_video_rooms, new_room_decoration_ui + setting_default
feature_group_calls ergaenzt. Discord-Raumliste war schon beidseitig an.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Automatischer Rotations-PR (Gitea #46) darf seit der Topologie-Umstellung
nicht mehr auf dem Mirror gemergt werden - Branch vom Mirror geholt und
hier kanonisch gemergt; der Push-Mirror traegt den Stand zurueck.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Das prometheus-node-exporter-HelmRelease (hostNetwork) kollidierte auf
dem Host mit dem etablierten systemd-Exporter auf Port 9100 und konnte
seit Anlage nie binden (4883 Restarts, CrashLoopBackOff). Host-Metriken
kommen weiterhin vom systemd-Dienst, den CFGMON direkt via 10.0.0.2:9100
scrapt - das DaemonSet haette sie nur dupliziert. Alloy-Scrape auf den
verwaisten Service ebenfalls entfernt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der Service matrix-stack-synapse-main exponiert 8008/9093/8080/9001
(synapse-metrics = 9001) - Port 9000 existiert nicht, der Scrape war
seit Anlage tot. Aufgedeckt durch den ersten TargetDown-Alarm des neuen
Alertings (gitops#32) direkt nach dem Deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
Der deploy-on-push-Check (YAML-Manifest-Pruefung, SOPS-ENC-Kontrolle) laeuft
jetzt als leichter Job in der Lab-GitLab-CI; milestone-release.yml war seit
jeher toter Code (kein Runner, Releases wurden manuell per API erstellt,
siehe Issue #33). Flux bleibt unberuehrt - es zieht weiterhin vom
Gitea-Mirror. Damit verliert der Gitea-Runner builder-1 seinen letzten
aktiven Konsumenten.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documentation audit across the repo found several places still describing
finished work as pending or in-progress (Authentik Stage 2, Element Call
fork, NetworkPolicies in TASKS.md's own Next Steps section, the Boje
troubleshooting entry). Also moves CLAUDE.md from the untracked parent
directory into the repo root and brings its content up to date, and
documents the new host-config/ pattern in README.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
unattended-upgrades was already active on the host, just never documented
or closed. Adds a generic, reusable systemd timer + script that fires
before the daily update window and notifies via email and a Matrix thread
reply if any packages are actually pending - reusing the mas-cli bot
account pattern established for Draupnir.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
First full rebuild deployed to production (previous releases used the
widgets/element-call/-only patch workaround, fixed but not cut over in
Issue #12). Includes the new client-side scan hooks (DecryptFile.ts,
ContentMessages.ts) that cover encrypted rooms, which the server-side
Synapse module (Issue #19) structurally cannot see.
Synapse's own media-scanning module (Issue #19) can never see E2EE
attachment content - a structural limit, not a bug. This adds a small
standalone HTTP wrapper around the same ClamAV instance, reachable
from browser JS at /_scan, so the ThreadNet-Web client fork can scan
plaintext both before encrypting/uploading and after downloading/
decrypting - covering both directions regardless of room encryption.
Auth via Synapse's own /whoami endpoint, no separate auth system.
Synapse runs on Twisted's reactor, not asyncio's event loop - the
original asyncio.open_connection/wait_for calls failed immediately
with "RuntimeError: no running event loop", silently fail-opening
every scan (confirmed live: EICAR test file passed through unscanned).
Rewritten using twisted.internet.endpoints.HostnameEndpoint/
connectProtocol and a custom Protocol for the INSTREAM conversation.
Deploys ClamAV and a small stdlib-only Synapse spam-checker module
implementing check_media_file_for_spam over clamd's INSTREAM protocol.
Unlike the originally-considered matrix-content-scanner proxy (which
needs client-side cooperation neither Element Web nor Element X
provide), this hooks Synapse's own module API directly - transparent
to every client for unencrypted media. No custom Synapse image needed:
the module is mounted via a ConfigMap onto PYTHONPATH using the ESS
chart's extraVolumes/extraVolumeMounts/extraEnv support. Fails open on
scanner errors so a ClamAV outage can't block all uploads.
Testing E2EE support for the management room per user request - marked
"not considered production safe" upstream, verifying no regressions
before deciding to keep it.
Confirmed by extracting dist/config.js from the actual image - v3.x's
getConfigPath() only checks --draupnir-config/--mjolnir-config CLI
flags, no longer NODE_CONFIG_DIR/node-config auto-discovery.
initialManager's automatic management-room creation (no manual room
setup needed) only works on v3.1.0+ - v2.9.0 crashed on first startup
trying to join a room that was never created.
Bot account registered via mas-cli, compatibility token issued and
stored via sops. Adds an explicit NetworkPolicy allow-rule since
Draupnir needs to reach Synapse's client-server API in-namespace,
which the existing default-deny policy would otherwise silently block.
Real testing (Safari + Firefox, fresh rejoins) confirmed VP9 selection
always falls back to VP8 - our fork's buildPublishOptions() forces
simulcast:true unconditionally, which breaks LiveKit's SVC negotiation
path that vp9/av1 require. This is the same failure mode originally
suspected in the 2026-07-28 incident; the "LiveKit handles this
automatically" assumption from doc research did not hold up in
practice. Reverting to the known-safe VP8/H.264/H.265 SFU allow-list
and widget image. Root cause documented in Issue #11 for a future,
scoped fix (conditional simulcast vs. scalabilityMode per codec).