Zweiter Anlauf des Upstream-Anschlusses (ADR-0022). Entfernt den Merge-Rest,
der rc.2 die Raumliste brach, und bringt einen typecheck-Job mit, den
docker_web als needs fuehrt - dieses Image ist das erste, das ohne bestandene
Typpruefung gar nicht haette entstehen koennen.
Abnahme steht aus, in dieser Reihenfolge: Raumliste laedt, ClamAV per Zip,
ClamAV per .png (der umgezogene Bild-Pfad), Call-Teilnehmerliste.
Rueckhebel: Tag zurueck auf v0.5.4.
react-soft-crash bei sorb (Rageshake 2026-08-19 15:17, Safari):
"Setting 'feature_room_list_sections' does not appear to be a setting."
aus SettingsStore.getValue in RoomListItemViewModel.generateItemSync - also
bei jedem Raumlisteneintrag.
Fehler in der Merge-Aufloesung von ADR-0022: Upstream hat den Labs-Schalter
feature_room_list_sections entfernt (Sektionen laufen jetzt ueber
RoomList.showSections). Settings.tsx hat Upstreams Fassung uebernommen, in
RoomListItemViewModel.ts blieb die alte getValue-Zeile daneben stehen.
Der Build konnte das nicht fangen: getValue nimmt einen String, der Fehler
entsteht erst zur Laufzeit.
Kandidat kommt nach dem Fix als rc.3 zurueck.
Kandidat, kein Release. Bringt den Merge aus ADR-0022 in Produktion, damit die
Abnahme an einem echten Client stattfinden kann.
Zu pruefen sind die zwei Patches, die der Merge verschieben musste:
ClamAV-Fehlermeldung im Bild-Pfad und die Call-Teilnehmerliste in der Raumliste.
Der Datei-Pfad (Zip) ist unberuehrt und diente heute als Ausgangswert - der
Scanner meldete die EICAR-Datei erwartungsgemaess zweimal, beim Senden und beim
Empfangen.
Rueckhebel: Tag zurueck auf v0.5.4.
Decision sorb. Hides the edit button beside the server name, so the homeserver can no
longer be switched through the UI, and the 401/403 login error now names the server
rather than staying generic.
Honest about its reach, in the comment as well as here: it is a surface restriction.
MatrixChat still takes hs_url from the query string in two registration flows without
consulting this setting, so a crafted link is unaffected. Against
GHSA-wrcp-5v3v-3j6v - open since 2026-07-20, affecting everything below 1.12.22 while
we run 1.12.17 - it narrows the way in without closing it. The update in management
#0099 remains the actual fix.
The matching line went into the desktop client separately, since that one carries its
own config.json.
Pipeline 518 today was red with zero jobs - a push where no rule matched, which
GitLab still turns into a pipeline and then marks failed. The standstill check
reported it correctly, and it was the only finding standing between the scheduled
checks and green.
Acknowledging it would have been the wrong move twice over: the entry would name one
pipeline id, so the next empty pipeline files a fresh finding, and the underlying
behaviour - red without a fault - is exactly what management #0104 is about. Rules
that prevent the empty pipeline remove the class instead.
schedule comes first deliberately. The daily canonization changes no paths, so a
changes-based rule alone would drop it.
Decision sorb. Measured basis rather than preference: four months of operation with
zero destinations, zero remote users and zero rooms with outside participation, while
the federation API answered publicly - the delegation routes it over 443, so 8448
being shut never mattered.
An empty list federates with nobody and one entry opens it for exactly that domain,
so the capability stays one line away rather than gone.
The comment records what must not be done instead, because it is not obvious and it
would look correct: blocking /_matrix/federation at the edge. lk-jwt-service verifies
OpenID tokens through /_matrix/federation/v1/openid/userinfo and reaches it over the
public name - no hostAliases, ClusterFirst DNS - so a path-level block kills group
calls. Synapse serves that endpoint without an X-Matrix signature, so the whitelist
does not touch it.
Caught while validating: the first version of this edit split the auto_join block,
moving auto_join_rooms_for_guests under federation. Functionally identical after the
fragments merge, wrong to read, and fixed before pushing - the diff is now 20 added
lines and nothing moved.
First egress rule in matrix, authentik and monitoring. It allows DNS, the cluster
ranges and the whole internet, and denies only 169.254.0.0/16 - link-local, where
Hetzner serves instance metadata unauthenticated to any pod.
Deliberately narrow. The textbook cut, 0.0.0.0/0 except RFC1918, would have severed
two things here, both over 10.0.0.3 on the private Hetzner network: Alloy writes
metrics and logs there, and the TURN rotation reaches Gitea through a hostAlias to
that address. Private ranges therefore stay open.
The payoff is modest and should be stated as such: measured from a pod, the service
answers with instance-id, hostname, region, MAC and network config, while userdata
and public-keys are empty. No credentials are exposed here, unlike the AWS case this
hardening usually targets. It costs nothing though, and it closes the class.
Two preconditions checked rather than assumed, because both are the usual way this
breaks: kube-system carries kubernetes.io/metadata.name so the DNS rule actually
matches, and the cluster is IPv4-only so 0.0.0.0/0 really does cover everything.
Rollback is deleting the one policy per namespace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pipeline 521: the variable is visible inside the job, the push to a throwaway ref
succeeded and the ref was removed again. Combined with the token's own properties -
Maintainer, write_repository only - the monthly canonization has what it needs on
2026-09-01.
Two things the probe itself taught, kept here rather than in the removed block: an
API-triggered pipeline has source "api", not "web", so the first attempt silently
skipped the job entirely; and this repo has no "pruefen" stage, which rejected the
pipeline outright. Both would have cost the same time again next year.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Runs only on a web pipeline started with TEST_CANONIZE=1, pushes a throwaway ref
and deletes it again, and never touches main. Removed after the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decision sorb, and it was already made in #0049: normal Authentik users read the
user documentation, admins are admins. The role model was implemented; the way in
was not. selfRegistration created an account on first login and autoEnrollGroups
was empty, so the account landed in no group at all - and since Guests is stripped
of every permission, the user saw nothing and was told nothing about why. That is
#0103, and it happened to a real person.
Admins stay manual: membership in "authentik Admins" arrives through the groups
claim and is not affected by this baseline. betrieb/* keeps its default deny, so
the separation #0049 verified end to end still holds - it only stops applying to
people who were never let in at all.
The lookup aborts if wiki-anwender is missing rather than silently enrolling into
nothing, which would reproduce the exact failure this fixes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observed rather than reasoned: after the leftover rotation branch was deleted on
both remotes, pipeline 492 still reported it. The runner recycles its workspace
under GIT_STRATEGY=fetch, and git fetch without --prune keeps remote-tracking refs
for branches that no longer exist upstream. The job would have kept naming a branch
nobody can find, indefinitely - and before the previous commit it would have kept
failing on it, with deletion offering no way out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The daily canonize_rotation job had been red for nine days. Cause: the branch
turn-secret-rotation-20260728-192656 was merged on Gitea back in July but never
deleted, so the job kept trying to merge it into a main that had moved on. Its
merge base is ancient, so the merge conflicts in ten files - not only docs but
coturn-secret.yaml, synapse-turn-secret.yaml and element-server-suite.yaml.
That made the failure worse than noise. The job's own advice is "resolve by hand",
and a careless resolution there rolls the TURN shared secret back to the July
value; Synapse and coturn would then disagree and TURN would be dead. main already
carries a newer rotation (2026-08-01 against 2026-07-28), so there was never
anything to gain from the merge.
The job now reads the SOPS lastmodified stamp - metadata, not a secret - from both
sides and skips a branch whose rotation is not newer than main's, naming it as
cleanup. It stays green while doing so, deliberately: AGENTS.md makes this red
pipeline the only alarm channel for the Gitea exception, and a pipeline that is red
every day for housekeeping is not an alarm any more. A genuine conflict still fails,
now with an explicit warning never to take the older secret.
Verified both directions against the real repository state, not just the happy path:
the leftover branch is detected as superseded, and with the roles swapped a real
rotation is still recognised as needing canonization.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On Safari LiveKit silently skipped the sender track swap, so the raw microphone
stayed on the wire regardless of the suppression level. The fork now verifies
and enforces the swap and states the outcome in the console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gate opens after the passed two-person acceptance: checkbox and slider are
back in the in-call audio settings. Rollback lever for any regression is the
gate in threadnet-call, not a deployment revert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships threadnet-call df4e5ee: the AI filter attaches to the microphone track
after publication with its own AudioContext on just that track. The feature
gate stays closed, so this behaves identically to v0.5.1 for every user; a
single test client opts in via two localStorage keys. The gate opens only
after the filter passes a two-person call - standing rule from #0054.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships threadnet-call dcc8643: the AI filter's off-path is byte-identical to
upstream again (no processor key, noiseSuppression untouched) and the feature is
hard-gated off until the webAudioMix decision. The gate also covers clients that
still have the setting enabled in localStorage. Regression tests pin both cases
and were demonstrably red on the broken code.
Acceptance is a real two-person call after the rollout; v0.4.3 remains one
tag-revert away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The console pinned it: with the filter on, LiveKit refuses the processor because
Element Call constructs the room without webAudioMix, so no local audio track
ever carries an AudioContext. With the filter off the same build does publish
its track, so the opt-out path itself is intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sorb asked for it: three hypotheses about the broken unmute were disproven from
the outside, so the browser console is the only remaining source. Calls stay
broken while this runs. Goes back to v0.4.3 as soon as the console is captured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Calls connect but no participant can unmute, and the SFU log shows not a single
published track. v0.5.0 is the first production image carrying the AI noise
suppression code in the audio capture path, and v0.4.3 is the last image calls
demonstrably worked on. Restoring service first; the cause is still open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships ADR-0018: DeepFilterNet3 as an opt-in filter in the call widget, default
off, checkbox plus slider, 35 % by default. The image now carries 23 MB of model
assets under /widgets/element-call/assets/dfn3/; they load when the user turns
the filter on, not on page load, so anyone leaving it off pays nothing.
The .7 package would have shipped a filter that was dead inside the widget and
nowhere else. Verified through the chain instead of trusting the green build:
npm package, node_modules, webpack output, and the CI artifact all carry the
assets at the path the widget requests. The last link — the running pod — gets
checked after this syncs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reporting worked but ended in silence: report_event.admin_message_md was unset,
so a user who reported content saw no indication of whether it reached anyone or
whom to follow up with. For a moderated community that is an open edge.
sorb's decision is route B — reports stay in the server's event_reports store and
are reviewed through Element Admin; Draupnir deliberately does not get server
admin rights, which polling would have required. The message therefore names a
person rather than promising an automatism, and @sorb is the only admin who can
actually see the reports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file still told readers the doc sources are served side by side as a
Docusaurus site at wiki.lab. That site never went live and the name does not
answer at all — Wiki.js inside this stack replaced it (ADR-0014). Documentation
pointing at something dead is worse than none, since it sends the next session
looking for a service that was retired.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Authentik's own uniqueness is case-sensitive, so 'Boje' and 'boje' pass as
distinct while Matrix treats them as the same localpart. ADR-0011 closed the
takeover vector with on_conflict:fail, but that only bites at login: the user
registers happily and fails later with no explanation. This policy answers where
the mistake is made.
Deliberately reads only prompt_data and never request.user — the stage runs in an
anonymous enrollment context, which is exactly what the previously attached system
policies crashed on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The running container was 4.10.0 while :latest had moved on to 4.17.2 — with
imagePullPolicy IfNotPresent the node keeps whatever it pulled once, so nobody
knew what was actually running and the next reschedule onto a fresh node would
have jumped seven minor versions silently. That is the concrete case #0052 is
about, and it also explains why the CVE scanner reported against a moving target.
Pinned to 4.17.2, which is both current and what :latest resolves to today, so the
scan results finally describe the thing that runs. The config uses only long-lived
core options (realm, use-auth-secret, relay-ip, cert/pkey), none of them removed
in that range. busybox in the init container goes 1.28 to 1.36, the version this
repo already uses elsewhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follows the management repo's shape (ADR-0013): CLAUDE.md is the one-line pointer
the group check looks for, AGENTS.md carries the project specifics unchanged. The
group-rules reference now points at management's AGENTS.md rather than its
CLAUDE.md, which had itself become a pointer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These files existed only on CFGMON, so rebuilding the host meant reconstructing
them from AAR prose (management #0027, W4 point 4). Follows the maintenance-notify
pattern: .example for anything carrying a secret, real files for the rest.
The private key stays out of the repo by design — it lives on the host and nowhere
else, and is reissued from the UDM if lost. The template carries the structural
values from ADR-0004 and the zone set from ADR-0017, with the reasoning inline:
why the direction is reversed, why AllowedIPs is narrow, why the port is 51841,
and why ping is the wrong reachability check.
Both files are derived from the ADRs rather than copied off the host, which the
README says plainly along with the redacted dump command to reconcile them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restores the Borg archives into a throwaway postgres inside the pod and passes
only when rows actually land — the pg_restore exit code is not proof, counted
rows are. Production is never touched; the repos are only read.
Automated rather than a documented cadence: a check nobody performs is the same
mistake as an untested backup, one level up. Runs on the 4th at 04:20, after the
nightly jobs. Verified manually before commit (synapse 31908 rows, MAS 16085,
wiki 251).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wiki.js auto-provisions SSO users in processProfile without a timezone, so
the DB column default (America/New_York) applies. Since there is no Wiki.js
image build pipeline, patch server/models/users.js at container startup to set
timezone: 'Europe/Berlin' on new-user creation. Idempotent (grep guard) and
fail-open (node starts even if the anchor is gone after an upgrade).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The content is German but was bound to the 'en' locale, the site default
language was English, and system accounts defaulted to America/New_York.
- ensure_locale: set default locale to 'de' (live via updateLocale) and
migrate all pages en->de via pages.migrateToLocale, then rebuild page tree
and search index only when something was migrated.
- Navigation tree now stored under locale 'de' so getTree finds it for the
now-German pages.
- ensure_timezones: set guest + admin system accounts to Europe/Berlin
(new auto-provisioned users still inherit the core-hardcoded NY default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default theme renders sidebar links with href=target verbatim, without
targetType or leading-slash handling. Page targets stored without a leading
slash resolved relatively (e.g. from /betrieb/x, 'Sicherheit' went to
/betrieb/betrieb/sicherheit -> 404), and the home item's empty target was a
dead link. Prefix page targets with '/' and point home at '/'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.