-
v2026.9.23
StableSome checks failedpublish-and-stage / decide (push) Successful in 10stag-release / release (push) Successful in 9sCI / release-scripts (push) Successful in 13ssecurity-scan / SBOM (trivy) (push) Successful in 14ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 19spublish-and-stage / build (backend) (push) Has been skippedsecurity-scan / JS SCA (npm audit) (push) Successful in 22spublish-and-stage / build (frontend) (push) Has been skippedpublish-and-stage / build (push) Successful in 0srelease-and-deploy / release (push) Successful in 26srelease-and-deploy / release-scripts (push) Successful in 13ssecurity-scan / Python SAST (bandit) (push) Successful in 29spublish-and-stage / deploy-staging (push) Failing after 14ssecurity-scan / Python SCA (pip-audit) (push) Successful in 47sCI / frontend-test (push) Successful in 1m20sCI / pre-commit (push) Successful in 1m29srelease-and-deploy / pre-commit (push) Successful in 1m30srelease-and-deploy / frontend-test (push) Successful in 1m24srelease-and-deploy / build (frontend) (push) Successful in 1m37srelease-and-deploy / build (backend) (push) Successful in 1m56srelease-and-deploy / build (push) Successful in 0sCI / backend-test (push) Successful in 4m48srelease-and-deploy / backend-test (push) Successful in 4m41sCI / e2e (push) Successful in 6m17srelease-and-deploy / e2e (push) Successful in 6m10srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 2m2sreleased this
2026-09-23 21:50:02 +00:00 | 70 commits to main since this releaseSpell check
- The browser's spell check now works in replies, new messages and notes.
Switch it off under Instellingen ("Spellingscontrole bij typen").
Postvak
- Fixed: a new message that already had replies appeared twice in an expanded
row. - Fixed: in dark mode a team logo with dark lines vanished into its dark
avatar circle. Avatars now keep their light-mode circle in both themes. - "Tot hier gelezen" no longer shows a toast with an undo that suggested the
read state could be put back; only a failure is reported. Its tour step now
says plainly what it does. - Fixed: "Tot hier gelezen" stayed disabled on a row whose only unread
message was your own (after "Markeer als ongelezen"). Your own posts past
the read cursor now count as unread, as they do in Mattermost.
Uitgaande post
- Fixed: a long link in the original message filled the whole two-line
preview and hid the text after it; it is now cut off on one line. - Fixed: the queued message showed raw markdown (
[tekst](url)) cut off at
120 characters; it now renders like the original above it. - A message that did not go out now shows on every page: a banner with a
link to Uitgaande post, and a count on its menu item. - A failed or cancelled message is never a dead end: "Openen" takes it back
as a draft, "Opnieuw verzenden" sends it again, "Verwijderen" lets it go.
Messages
- Fixed: bot posts that carry their content in message attachments showed
up empty. Waggle now renders those attachments below the message text.
Under the hood
- Merging is releasing: a push to
mainwith entries under Unreleased
promotes them to a dated section, tags it and deploys production. Staging
only runs under a release hold; seedocs/deployment.md.
Container images
- The browser's spell check now works in replies, new messages and notes.
-
v2026.9.21.1
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 11ssecurity-scan / SBOM (trivy) (pull_request) Successful in 11ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 18ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 19ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 22ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 40stest-build / build (frontend) (pull_request) Successful in 1m1sCI / frontend-test (pull_request) Successful in 1m5stest-build / build (backend) (pull_request) Successful in 1m5stest-build / build (pull_request) Successful in 0sCI / pre-commit (pull_request) Successful in 1m17sCI / backend-test (pull_request) Successful in 2m50ssecurity-scan / SBOM (trivy) (push) Successful in 12sCI / release-scripts (push) Successful in 12ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 16ssecurity-scan / JS SCA (npm audit) (push) Successful in 17ssecurity-scan / Python SAST (bandit) (push) Successful in 27ssecurity-scan / Python SCA (pip-audit) (push) Successful in 45sCI / pre-commit (push) Successful in 1m0sCI / frontend-test (push) Successful in 1m8spublish-and-stage / build (frontend) (push) Successful in 1m12spublish-and-stage / build (backend) (push) Successful in 1m19spublish-and-stage / build (push) Successful in 0sCI / e2e (pull_request) Successful in 5m43sCI / backend-test (push) Successful in 2m53spublish-and-stage / deploy-staging (push) Successful in 1m49sCI / e2e (push) Successful in 4m57srelease-and-deploy / release-scripts (push) Successful in 7srelease-and-deploy / release (push) Successful in 20srelease-and-deploy / pre-commit (push) Successful in 1m0srelease-and-deploy / frontend-test (push) Successful in 1m1srelease-and-deploy / build (frontend) (push) Successful in 1m8srelease-and-deploy / build (backend) (push) Successful in 1m15srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 2m43srelease-and-deploy / e2e (push) Successful in 4m52srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 2m3sreleased this
2026-09-21 09:30:31 +00:00 | 84 commits to main since this releasePostvak
- Fixed: an expanded row listed thread replies as new channel messages. With
Collapsed Reply Threads on, the channel window now asks Mattermost for root
posts only; replies stay with their thread. - Fixed: a Collapsed Reply Threads setting changed in Mattermost was never
read; Waggle asked for it at a route that does not exist and fell back to
the server default.
Under the hood
- The release scripts accept the same-day
.Ntag suffixdocs/deployment.md
prescribes;v2026.9.21.1would have failed the release job and never
deployed.
Container images
- Fixed: an expanded row listed thread replies as new channel messages. With
-
v2026.9.21
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 9ssecurity-scan / SBOM (trivy) (pull_request) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 17ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 22ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 42sCI / release-scripts (push) Successful in 13ssecurity-scan / SBOM (trivy) (push) Successful in 14ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 20ssecurity-scan / JS SCA (npm audit) (push) Successful in 22ssecurity-scan / Python SAST (bandit) (push) Successful in 27stest-build / build (frontend) (pull_request) Successful in 1m5sCI / pre-commit (pull_request) Successful in 1m5sCI / frontend-test (pull_request) Successful in 1m7stest-build / build (backend) (pull_request) Successful in 1m8stest-build / build (pull_request) Successful in 0ssecurity-scan / Python SCA (pip-audit) (push) Successful in 46spublish-and-stage / build (frontend) (push) Successful in 1m8spublish-and-stage / build (backend) (push) Successful in 1m17spublish-and-stage / build (push) Successful in 0sCI / pre-commit (push) Successful in 1m55sCI / backend-test (pull_request) Successful in 2m55spublish-and-stage / deploy-staging (push) Successful in 1m47sCI / e2e (pull_request) Successful in 4m57sCI / frontend-test (push) Successful in 55sCI / e2e (push) Successful in 13m1sCI / backend-test (push) Successful in 14m4srelease-and-deploy / release-scripts (push) Successful in 7srelease-and-deploy / release (push) Successful in 18srelease-and-deploy / pre-commit (push) Successful in 53srelease-and-deploy / frontend-test (push) Successful in 58srelease-and-deploy / build (frontend) (push) Successful in 1m5srelease-and-deploy / build (backend) (push) Successful in 1m11srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 2m38srelease-and-deploy / e2e (push) Successful in 4m41srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m51sreleased this
2026-09-20 22:17:23 +00:00 | 89 commits to main since this releasePostvak
- Fixed: an empty Postvak for anyone in more than 60 channels. Mattermost
pages/users/me/channel_members, and the client read only the first page.
Under the hood
docs/deployment.mdsays how to scale to zero on ZAD: there is no replica
count, only the "Deployment slapen" button, which the CLI does not expose.
Container images
- Fixed: an empty Postvak for anyone in more than 60 channels. Mattermost
-
v2026.9.20
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 9ssecurity-scan / SBOM (trivy) (pull_request) Successful in 11ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 21ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 35ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 40sCI / release-scripts (push) Successful in 10ssecurity-scan / SBOM (trivy) (push) Successful in 13ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 19ssecurity-scan / Python SAST (bandit) (push) Successful in 23stest-build / build (backend) (pull_request) Successful in 1m10ssecurity-scan / JS SCA (npm audit) (push) Successful in 38sCI / frontend-test (pull_request) Successful in 1m18stest-build / build (frontend) (pull_request) Successful in 1m21ssecurity-scan / Python SCA (pip-audit) (push) Successful in 46stest-build / build (pull_request) Successful in 0sCI / pre-commit (pull_request) Successful in 1m2spublish-and-stage / build (backend) (push) Successful in 1m29spublish-and-stage / build (frontend) (push) Successful in 1m33sCI / pre-commit (push) Successful in 1m33spublish-and-stage / build (push) Successful in 0sCI / frontend-test (push) Successful in 1m42sCI / backend-test (pull_request) Successful in 3m54spublish-and-stage / deploy-staging (push) Successful in 1m52sCI / backend-test (push) Successful in 4m9sCI / e2e (pull_request) Successful in 5m41sCI / e2e (push) Successful in 5m3srelease-and-deploy / release-scripts (push) Successful in 8srelease-and-deploy / release (push) Successful in 18srelease-and-deploy / frontend-test (push) Successful in 58srelease-and-deploy / build (backend) (push) Successful in 1m13srelease-and-deploy / pre-commit (push) Successful in 1m16srelease-and-deploy / build (frontend) (push) Successful in 1m23srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 3m41srelease-and-deploy / e2e (push) Successful in 5m31srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m32sreleased this
2026-09-20 19:13:53 +00:00 | 93 commits to main since this releaseNote when rolling out
- This release logs every user out. The production session cookie is
renamed to__Host-waggle_sessionand the old name is no longer read, so
every existing session ends at deploy. WAGGLE_COOKIE_NAMEis nowWAGGLE_COOKIE_NAME_BASE; the old variable is
ignored, so move a value set there.- Not rolling-safe. The migration renames the
trackvalueinboxto
todo; during a rolling deploy the old pod still writesinboxinto a
column that no longer accepts it. Scale the backend to zero replicas, let
the migration run, then start the new image. There is no in-place rollback.
Pagina: Postvak
/is now the Postvak: your unread Mattermost channels, one row per channel,
the threads inside it counted in. Channels you only follow along with go to
"Meelezen" (/meelezen,g m), between "Nog te doen" and "Leeslijst".- A row expands into one chronological stream, opening at your first unread
message; a thread is one darker block with its replies indented. "Toon
eerdere berichten" pages back through read history up to 200, and further
unread messages load as you reach the end. - Reading marks nothing read. "Tot hier gelezen" covers only what passed the
viewport, bounds the channel and each thread on their own, says what it
leaves ("3 blijven ongelezen") and can be undone. There is no "alles
markeren als gelezen". - Hover or focus a message for its action bar. "Antwoord" opens the card
composer inline - formatting, bijlagen, @-mentions, thread or channel,
scheduling - and the emoji button reacts straight away. Neither makes a card:
a reply shows up in "Uitgaande post" and nowhere else. - The message menu puts it on a list ("Doe nu", "Backlog", "Bubble up" with a
time, or the leeslijst) or opens it in Mattermost. Listing it saves it in
Mattermost too, and reuses the card it already had, including a finished one. - By keyboard:
j/kwalk the rows and everything an open row shows,r
answers the focused message,mopens it in Mattermost, Escape closes the
reply field.→/←step into and out of a row too long to walk. The
shortcut list is regrouped by what has focus rather than by page. - A Meelezen channel you are named in also appears in the Postvak while the
mention is unread, and stays in Meelezen too. - Every channel lands in the Postvak; one you muted in Mattermost starts in
Meelezen. The row's menu ("Berichten afleveren in") moves it either way, and
resetting hands the choice back to your Mattermost sidebar. - A row names its team beside the channel, with the team's logo or its
initials: every Mattermost team has a Town Square, so the channel name alone
identifies nothing. - With more than one linked server both pages get a tab bar: "Alle servers",
which groups rows per server, plus one tab per server with its unread count,
remembered per browser. "Groepeer Postvak en Meelezen per team"
(Instellingen, off by default) groups by team instead. - A message is drawn by the same component a card uses, attachments and all,
and one you wrote yourself no longer counts as unread. - Without a linked Mattermost account the Postvak and Meelezen now show the
"Welkom bij Waggle" block instead of a one-line hint:/is the Postvak, so
it is the first thing a new account sees. Its text no longer says "bewaarde
berichten", which described only Nog te doen.
Pagina: Nog te doen
- The Inbox is now called "Nog te doen" and lives at
/nog-te-doen.
Menu
- The "Menu" button stays in the header row and scrolls away with it at every
width, without a frame: nothing floats over the page anymore.
Fixed
- A card showed its team's URL slug ("digitale-overheid") instead of its name
("Digitale Overheid"). - A scheduled message no longer fails for good when its Mattermost server
cannot be reached at send time. A refused base_url (the SSRF guard also
refuses a name that does not resolve) or an unreadable token now takes the
backoff-retry path instead of settling as "geen Mattermost-account
gekoppeld"; an unreadable token used to leave the row stuck in "sending"
until a restart failed it. A send that does run out of retries now names
which of the two it was, instead of always blaming an unreachable server. - A permalink to a DM or group message pointed at
/pl/{id}, which is not a
route - Mattermost read "pl" as a team name and showed its team-not-found
page. It goes through/_redirect/pl/{id}now. - The composer toolbar no longer drops the emoji button onto a second row. Its
overflow breakpoints were narrower than the buttons actually measure, so the
Postvak's inline composer (600px of toolbar, 610px of buttons) wrapped instead
of collapsing a group; the numbers are now measured, per toolbar variant. - Two Postvak rows from different Mattermost accounts could share a
kind-id
key, which gave Vue duplicate keys and lit up two rows at once during the
tour. A row's identity now leads with its account id.
Security
- A live session is re-checked at Keycloak every five minutes
(WAGGLE_OIDC_REVALIDATE_INTERVAL_SECONDS), so a disabled or deleted user
loses access without waiting for the cookie to go idle - Keycloak sends no
back-channel logout for that. Login now asks foroffline_access. A Keycloak
outage is tolerated for a day (WAGGLE_OIDC_REVALIDATE_GRACE_SECONDS) and
never ends a session. - Failed security events reach the audit trail: a refused login, the outcome of
a back-channel logout, and use ofwaggle rotate-keys. - Refusals a caller can repeat at will - a rejected Origin, a rate-limit hit, a
stale session cookie, an SSRF-blocked URL - are logged but never audited: a
flood must not write a row per request. audit_events.ip_hashis now an HMAC under its own HKDF domain
(waggle-iphash-v1) instead of a hash over the raw secret. Existing rows keep
the old digest and stop correlating; the raw addresses to re-hash them were
never stored.- Audit retention is now per kind of record: use of a system utility is kept
180 days (WAGGLE_AUDIT_UTILITY_RETENTION_DAYS, BIO2 8.18.02), everything
else stays on 90.docs/security.mdrecords the reasoning and lists which
logs we generate. - The SSRF guard on a Mattermost server URL checks every resolved address of
both families, so an IPv6 literal (https://[::1]/) no longer slips past.
A hostname that does not resolve is refused, and carrier-grade NAT, NAT64,
6to4 and Teredo are named explicitly. - An hourly
session_purgeloop deletesoidc_sessionsrows idle longer than
the session-cookie window, so an abandoned login no longer keeps its
encrypted Keycloak tokens at rest. - The session and PKCE-state cookies carry the
__Host-prefix in production,
so a sibling on the shared parent domain can no longer plant one of the same
name. Development keeps the bare name, which plain HTTP forces. - The channel and team name cache is keyed per account, not only per server: a
shared key answered a private channel's name to any caller that reached the
cache. The account part is an HMAC under a newwaggle-cachekey-v1domain. - Outbound URLs no longer reach the pod log:
httpxandhttpcorelog at
WARNING, and an HTTP error is sanitised before it is logged and redacted
in the traceback. docs/security.mdrecords what the client address is worth: the ZAD router
passes a client-suppliedX-Forwarded-Forthrough unchanged, so the audit
origin is an indication rather than evidence and the pre-login rate limiter's
IP fallback can be rotated around. Raised with the platform team.- A failed login no longer writes an audit row per attempt: the throttle keys
on the reason instead of the client-supplied origin, which was rotatable. The
row that does get written now carries how many occurrences it stands for and
how many distinct origins they came from, per process. The addresses are not
recorded: the caller picks them, so one could name an uninvolved third party. POST /api/auth/backchannel-logoutrefuses an implausible token (wrong
issuer, missingevents/jti, anonce, a staleiat, an oversized body)
and a replayedjtibefore it verifies any signature.
Under the hood
- Seven more ruff rulesets on the backend:
PLE,PGH(no barenoqaor
type: ignore),T10,RSE,ISC004,PERFandFURB. Fifteen findings,
one of them real - a.replace("Z", "+00:00")thatfromisoformathandles
by itself - and no rule suppressed to get there. - Fixed: the
JS SCA (npm audit)job went red on about 2% of all runs, because
npm does not retry the POST its audit endpoint needs. It retries now, unless
npm actually returned a report - a finding still fails on the spot. - The ruff pre-commit hooks run
uv run --project backend ruffinstead of a
pinned mirror that sat eight minor releases behindpyproject.toml- CI
lints with the versionjust lintuses. Renovate's pre-commit manager is
opted into, so the remaining hook pins stop freezing. - A released
## [vX]changelog section is frozen: CI compares every tagged
section againstgit show <tag>:CHANGELOG.mdand fails on a difference. The
Forgejo release is generated at tag time, so an edit afterwards left the
published notes and the file disagreeing with no warning anywhere. - Fixed: the
release-scriptsCI job failed whenever a second run checked out
the workspace mid-test. The section tests read a snapshot ofCHANGELOG.md
instead of the file under them, and the retry that papered over it is gone. - Scheduling a message is 120 an hour rather than 30, on the card route and the
Postvak one alike, and a thread's messages 120 a minute: expanding one row
spends a request per thread in it. - Renovate keeps marked behind dashboard approval while
markedjs/marked#4098 is
open: every new release stays listed there, without a weekly PR that our own
regression test is only going to fail. - CI runs on node 24, like the frontend image. Renovate dropped the
setup-node
bump silently: its digest pin now covers actions only, notnode-version. - The session cookie is an OpenAPI security scheme (
SessionCookie), so a
generated client can see that authentication exists. A test asserts which
operations are public. - The axe-core e2e gate now runs the full default ruleset (WCAG 2.0/2.1 A and
AA plus best practice) on every route, instead of three named rules. Measured
clean on all of them; the narrower set was hiding what it did not check. GET /api/mm/team-image/{team_id}proxies a team logo with the stored token,
like the avatar route, but only for a team you are in. A team without a logo
answers 404, never a placeholder: the caller needs the error to fall back to
initials.POST /api/mm/reactionsandDELETE /api/mm/reactions/{post_id}/{emoji}
react to a message without a card: account-scoped, with the same
membership-before-lookup check as the rest of the Postvak.POST /api/mm/replyandPOST /api/mm/reply/scheduledsend a Postvak reply
now or later, without a card. The thread root travels on the outgoing row in a
new nullableroot_id, which is what a scheduled reply needs at fire time; a
card row still resolves its root from the card. See WDR-0011.- New
/api/mm/unreadendpoints back the Postvak: one row per unread channel,
carrying the threads inside it, their counts and its sidebar-seeded priority,
the unread window of a channel or thread, and the two bounded read-cursor
writes. A channel id in a
request is checked against the caller's own memberships before any lookup;
rendering never marks anything read. - The Postvak listing reuses the composer's 300-second per-account cache for the
team list, sidebar categories and group names: a poll costs 10 Mattermost
calls instead of 36 for a five-team account, which keeps a shared MM rate
limit out of reach. POST /api/cards/from-postpromotes a message. It saves the post in
Mattermost before inserting the card, because an active card outside the
saved list is auto-completed by the next sync run, and unsaves again when the
insert fails. On conflict the existing card is revived rather than left
alone: a no-op would strand another saved post on every repeat promote.- A Mattermost card leaving "Voltooid" is saved in Mattermost again, which
closes a gap that predates the Postvak: completing a card unsaves it, so
dragging it back out left Waggle showing work whose source side was gone. PUTandDELETE /api/mm/unread/triagestore and clear a per-channel triage
override in the newtriage_rulestable. Absence of a row is meaningful, so a
rule is only ever written where the user disagreed with the sidebar;
resolution reads channel rule, team rule, sidebar seed, feed. The rules
travel in the export bundle.- Postvak rows for a DM or group conversation carry the participants' user ids,
so the page can draw their avatars. They come from the lookups the row's name
already needed, at no extra Mattermost call.
Container images
- This release logs every user out. The production session cookie is
-
v2026.9.19
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 10ssecurity-scan / SBOM (trivy) (pull_request) Successful in 13ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 21ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 22ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 27ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 46sCI / pre-commit (pull_request) Successful in 1m20stest-build / build (backend) (pull_request) Successful in 1m13stest-build / build (frontend) (pull_request) Successful in 1m7stest-build / build (pull_request) Successful in 0sCI / frontend-test (pull_request) Successful in 1m28sCI / backend-test (pull_request) Successful in 3m25sCI / e2e (pull_request) Successful in 5m27sCI / release-scripts (push) Successful in 12ssecurity-scan / SBOM (trivy) (push) Successful in 15ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 21ssecurity-scan / JS SCA (npm audit) (push) Successful in 23ssecurity-scan / Python SAST (bandit) (push) Successful in 25ssecurity-scan / Python SCA (pip-audit) (push) Successful in 44sCI / pre-commit (push) Successful in 1m2sCI / frontend-test (push) Successful in 1m7spublish-and-stage / build (frontend) (push) Successful in 1m12spublish-and-stage / build (backend) (push) Successful in 1m22spublish-and-stage / build (push) Successful in 0sCI / backend-test (push) Successful in 2m36sCI / e2e (push) Successful in 5m3srelease-and-deploy / release-scripts (push) Successful in 6srelease-and-deploy / release (push) Successful in 18srelease-and-deploy / pre-commit (push) Successful in 54srelease-and-deploy / frontend-test (push) Successful in 57srelease-and-deploy / build (frontend) (push) Successful in 1m10srelease-and-deploy / build (backend) (push) Successful in 1m16srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 3m30srelease-and-deploy / e2e (push) Successful in 5m21srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m58spublish-and-stage / deploy-staging (push) Successful in 4m44sreleased this
2026-09-19 20:01:14 +00:00 | 139 commits to main since this releaseComposer
- Bold no longer flickers while you type it: a space before the closing
**
moves outside once you leave the word, and bolding a selection leaves a
trailing space out. - The caret sits on the insertion point instead of over the next letter, and
against the URL at the end of a link; the open-link badge is one character
you step over. - Fixed: quotes line up again. Wrapped lines align with the first, the indent
follows the depth with or without a space after>, and a quote past the
first 3000 characters gets its left bar.
Fixed
- Text after an empty nested quote (
>>on its own line) disappeared from a
rendered message; marked is pinned to 18.0.8, the last release without it. - Video previews play in production again: the nginx CSP had
media-src 'none', now'self'. A test holdsdefault.confto what the app
needs.
Under the hood
- Images are published to
ghcr.iotoo and ZAD deploys from there, since
code.overheid.nl grants anonymous callers no pull scope. Every deploy first
checks the images are anonymously pullable. - A push to
maindeploys its:<sha>image to a ZAD staging deployment. - Fixed: ZAD deploys hit GitHub's API rate limit now and then; the deploy jobs
pin uv forzad-actions. - NLDD design system 0.8.90. Field errors moved to
nldd-validation-list, as
nldd-form-field-error-textis gone. - CI: e2e runs on two workers with a 60s test timeout; backend test output is
quieter. - Dependency bumps: uv 0.12.17, Renovate 44.103.2, zad-actions 4.2.0, node
24.21, nginx-unprivileged 1.31.6 and fresh python, postgres and keycloak
digests; backend authlib 1.8, alembic 1.20, cachetools 7.2, uvicorn 0.53,
sqlalchemy 2.0.54, cryptography 50.0.1, pydantic 2.13.5, ruff 0.16.8, ty
0.0.82; frontend vue 3.5.43, vue-router 5.3.1, @tanstack/vue-query 5.103,
dompurify 3.4.15, markdown-it 15.0.2, Playwright 1.63, vitest 5, vite 8.3,
eslint 10.11.
Container images
- Bold no longer flickers while you type it: a space before the closing
-
v2026.8.17
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 8ssecurity-scan / SBOM (trivy) (pull_request) Successful in 9ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 17ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 17ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 40stest-build / build (frontend) (pull_request) Successful in 58sCI / pre-commit (pull_request) Successful in 59stest-build / build (backend) (pull_request) Successful in 1m5sCI / frontend-test (pull_request) Successful in 1m6stest-build / build (pull_request) Successful in 0sCI / backend-test (pull_request) Successful in 2m34sCI / e2e (pull_request) Successful in 3m46sCI / release-scripts (push) Successful in 6ssecurity-scan / SBOM (trivy) (push) Successful in 11ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 14ssecurity-scan / JS SCA (npm audit) (push) Successful in 17ssecurity-scan / Python SAST (bandit) (push) Successful in 19ssecurity-scan / Python SCA (pip-audit) (push) Successful in 39sCI / pre-commit (push) Successful in 1m1spublish-main / build (frontend) (push) Successful in 1m0sCI / frontend-test (push) Successful in 1m5spublish-main / build (backend) (push) Successful in 1m14spublish-main / build (push) Successful in 0sCI / backend-test (push) Successful in 2m34sCI / e2e (push) Successful in 3m49srelease-and-deploy / release-scripts (push) Successful in 5srelease-and-deploy / release (push) Successful in 20srelease-and-deploy / pre-commit (push) Successful in 57srelease-and-deploy / build (frontend) (push) Successful in 1m0srelease-and-deploy / frontend-test (push) Successful in 1m0srelease-and-deploy / build (backend) (push) Successful in 1m10srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 2m31srelease-and-deploy / e2e (push) Successful in 3m42srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m56sreleased this
2026-08-17 19:38:04 +00:00 | 161 commits to main since this releaseReplying in the thread or in the channel
- The chevron next to "Verzend" opens one panel: the destination (thread or
channel) above the scheduling options. The button then reads "Verzend in
kanaal", and the choice returns to the thread every time. - "Markeer kaart als voltooid na verzenden" now applies to "Verzend" too, not
only to a scheduled send. The card completes once the send has gone out, so a
failure or an undo leaves it open. - A scheduled message keeps its destination, and Uitgaande post names it.
- A channel is named in words rather than with a sigil: "In kanaal Town
Square", "Aan @noor". The card breadcrumb drops its "#" too, so a
screenreader hears "kanaal X" instead of "#x". - Fixed: Escape in the scheduling popover closed the whole card.
Preview for text attachments
- A markdown attachment opens in a preview instead of downloading, with a
"Weergave"/"Broncode" switch. - Other text files (.txt, .csv, .json, code, logs) open straight in the source
view, syntax-highlighted where a grammar exists. Classified on the extension
too, because Mattermost reports many of them as octet-stream. - Above 1 MB the preview asks first and names the size; past 5 MB it fetches
only the beginning, so a big file is never transferred whole. - HTML, PDF and text previews now share one window instead of three
hand-rolled ones.
Cards load faster, and show a skeleton
- A loading card shows placeholder bars instead of a half-filled card, and its
quick actions and status tags wait for the message. - Takes it longer than a second, an activity indicator covers and dims the
list. Both follow NLDD's loading guidelines, which rule out a shimmer. - A message that fails to load says "Bericht kan niet worden geladen" instead
of waiting on skeleton bars forever; the card's actions stay usable. - Channel and team labels come from two collection endpoints instead of one
call per channel: four round trips per linked server, whatever the number of
cards. A few hundred cards took over ten seconds to paint before. - Those labels are now cached for an hour across requests; the old per-client
cache never hit, because the client is built fresh per request. - Fixed: a Mattermost token expiring during team lookup read as a server error
rather than a token problem.
Reading the messages around a card
- Opening the panel no longer hides the message you were reading.
- One centred "Berichten laden..." indicator instead of a spinner and a label
scattered across the width, and the panel settles once instead of twice. - A load-more button names its direction while it loads instead of shrinking to
"Laden...".
Under the hood
- An unreachable Mattermost no longer costs one hanging avatar request per
author: the first failure is remembered for a minute, the timeout drops from
15 to 3 seconds, and an upstream failure reads as 502 rather than 500. - An avatar reply that is not an image is refused rather than replaced by a
transparent pixel, which showed as an empty circle where initials belong. - The dev Mattermost mock is installed at startup instead of being reached for
per request:waggleno longer namesdevtoolsin the request path. - The mock can fake a per-call latency (
WAGGLE_DEV_MM_MOCK_LATENCY_MS),
answersposts/idsfor ids it never created, serves/users/me/channelsand
honoursRange. - The dev seed writes refs-only cards like production, instead of a snapshot
nothing reads that showed as one repeated message on every card. - Renovate 44.31.0; fresh nginx-unprivileged digest for the frontend image.
Container images
- The chevron next to "Verzend" opens one panel: the destination (thread or
-
v2026.8.16
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 7ssecurity-scan / SBOM (trivy) (pull_request) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 16ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 18ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 37stest-build / build (frontend) (pull_request) Successful in 56sCI / pre-commit (pull_request) Successful in 56sCI / frontend-test (pull_request) Successful in 1m1stest-build / build (backend) (pull_request) Successful in 1m28stest-build / build (pull_request) Successful in 0sCI / backend-test (pull_request) Successful in 2m20sCI / e2e (pull_request) Successful in 3m28sCI / release-scripts (push) Successful in 6ssecurity-scan / SBOM (trivy) (push) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 14ssecurity-scan / JS SCA (npm audit) (push) Successful in 15ssecurity-scan / Python SAST (bandit) (push) Successful in 16ssecurity-scan / Python SCA (pip-audit) (push) Successful in 34sCI / pre-commit (push) Successful in 56spublish-main / build (frontend) (push) Successful in 56sCI / frontend-test (push) Successful in 1m6spublish-main / build (backend) (push) Successful in 1m10spublish-main / build (push) Successful in 0sCI / backend-test (push) Successful in 2m19sCI / e2e (push) Successful in 3m26srelease-and-deploy / release-scripts (push) Successful in 6srelease-and-deploy / release (push) Successful in 18srelease-and-deploy / pre-commit (push) Successful in 57srelease-and-deploy / frontend-test (push) Successful in 58srelease-and-deploy / build (frontend) (push) Successful in 1m9srelease-and-deploy / build (backend) (push) Successful in 1m13srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 2m19srelease-and-deploy / e2e (push) Successful in 3m25srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m49sreleased this
2026-08-16 21:02:14 +00:00 | 174 commits to main since this releaseNote when rolling out
- The migration converts
cards.source_data,cards.notesand
outgoing_posts.contentto encrypted bytea in place. Not rolling-safe:
stop the old pod before the new one boots, and take a database backup
first - reading the converted columns afterwards requires the
WAGGLE_SECRET_KEYthe migration ran with. - A second migration then strips the message, author and attachments out of
cards.source_data. One-way by design: the downgrade cannot put them back.
Link a channel with ~
- Typing
~offers your channels, most recently linked first, and inserts a
reference to the one you pick. - Channels from your other teams are in the list too, marked with their team.
They get a full link, because Mattermost only resolves a~nameinside the
team of the message. @in a new message without a recipient now says "Kies eerst een kanaal bij
Aan" rather than inviting you to keep typing.
Release notes in the app
- Returning users get a "Nieuw in Waggle" banner above every page, folded shut,
listing the releases they missed by date. Dismissing it is permanent and
follows the account, not the browser. - Every missed release is in that one expand. A button underneath fetches the
releases before them, one at a time. - Notes are Dutch markdown per release in
frontend/src/content/releases/; a
release without a file shows no banner, and a new account sees nothing. The
release job fails whileunreleased.mdhas not been renamed to its version.
Under the hood
- Message snapshots, notes and outgoing drafts are encrypted at rest:
Fernet under two new HKDF domains, covered bywaggle rotate-keys. - A card stores references only - post, channel and permalink. The message,
author and attachments come from Mattermost per request, so an edited post
no longer needs a resync and nothing of it sits in the database. - A card whose post Mattermost no longer has reads "Bericht is niet meer
beschikbaar in Mattermost" and keeps its status, planning and notes. If the
fetch itself fails, the list says so once, with "Opnieuw proberen". - Cache and log hardening: API responses default to no-store, attachments cache
only as inline images for an hour, typed search text moved from URLs into POST
bodies, and the production access log is off. - Error paths stopped echoing content: database errors hide bind parameters,
422 responses drop the submitted input, the rate limiter logs a hashed key. - Logout clears the browser's cache and storage (
Clear-Site-Data) plus
localStorage traces;sync_logsage out after 90 days. - The pod writes its own scrubbed request log (status, duration, pseudonymous
user/session ids; no IPs or query strings) and sessions record
last_seen_atfor incident reconstruction. - NLDD design system 0.8.82. Enter submits the account forms again, the
retention dropdowns are now named by the label above them instead of by a
summary that shared no words with it, and the import success notice is an
nldd-bannerrather than a hand-rolled one. - Postgres 17 digest bumped to
e384114.
Container images
- The migration converts
-
v2026.8.15
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 6ssecurity-scan / SBOM (trivy) (pull_request) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 14ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 19ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 36sCI / pre-commit (pull_request) Successful in 56stest-build / build (frontend) (pull_request) Successful in 59sCI / frontend-test (pull_request) Successful in 59stest-build / build (backend) (pull_request) Successful in 1m5stest-build / build (pull_request) Successful in 0sCI / backend-test (pull_request) Successful in 2m17sCI / e2e (pull_request) Successful in 3m24sCI / release-scripts (push) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 23ssecurity-scan / Python SAST (bandit) (push) Successful in 37ssecurity-scan / SBOM (trivy) (push) Successful in 13ssecurity-scan / JS SCA (npm audit) (push) Successful in 29ssecurity-scan / Python SCA (pip-audit) (push) Successful in 53srelease-and-deploy / release (push) Successful in 35srelease-and-deploy / release-scripts (push) Successful in 11spublish-main / build (frontend) (push) Successful in 2m13sCI / pre-commit (push) Successful in 2m42srelease-and-deploy / frontend-test (push) Successful in 2m22spublish-main / build (backend) (push) Successful in 3m26spublish-main / build (push) Successful in 0srelease-and-deploy / build (frontend) (push) Successful in 2m57srelease-and-deploy / pre-commit (push) Successful in 3m21srelease-and-deploy / build (backend) (push) Successful in 3m47srelease-and-deploy / build (push) Successful in 0sCI / frontend-test (push) Successful in 2m17sCI / backend-test (push) Successful in 9m15sCI / e2e (push) Successful in 9m42srelease-and-deploy / backend-test (push) Successful in 8m57srelease-and-deploy / e2e (push) Successful in 8m48srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m49sreleased this
2026-08-15 20:19:50 +00:00 | 183 commits to main since this releaseThe composer edits markdown as text
- Reply, new message and notes run on the design system's editor instead of
Tiptap. You edit the markdown itself: the syntax shows on the line you are on
and is hidden elsewhere, the ``` of a code block included. - Tab nests a list item and indents inside a code block, Shift+Tab reverses it,
and Escape leaves the field before a second Escape closes the window. - A long message keeps the toolbar and the send button in view: the typing
surface grows to twenty lines, then scrolls. - The link bar lost its "Openen" button - the editor shows one beside the link
the cursor is in. - Fixed along the way, most visibly: the reply box had no accessible name,
pasting a screenshot over selected text deleted the text, the emoji list and
picker opened off screen on a scrolled page, and Escape from the link bar's
buttons closed the whole card. The rest is ingit log. - The toolbar's line markers (list, task, quote, heading, code block) now work
on an empty line, switch to another type, and put the cursor after the
marker instead of in front of it.
Menu and page titles
- The menu now sits behind a labeled "Menu" button: fixed top right on a wide
screen, beside the wordmark on a phone. Its items are real links
(cmd/ctrl-click opens a new tab), with the overview pages grouped under
"Overzichten". - The wordmark is a plain link to the Inbox above the page, and it scrolls
away with the content. - Inbox, Leeslijst and Instellingen have a visible page title, and the browser
tab follows the page ("Inbox - Waggle"). - Every page shares one left edge: titles, search, cards and the settings
sections all follow the design system's page gutter. - The inbox tour lost its menu step, and no longer jumps while the banners
load, ends on a click beside it, races through steps, or hides its card
steps under the modal.
Channel window
- Every root in the channel window now shows its reply count and unfolds in
place - until now only the saved message's own thread did.
Fixed
- Moving a card within its own list no longer drifts a slot off after half a
dozen moves into the same spot: positions are now also redistributed on a
reorder, not only when a card changes list. - A card moved with the keyboard now scrolls fully back into view instead of
stopping half under the fold. - Text in a dialog is the content colour again instead of pure black or white.
A native<dialog>carries the UA'scolor: CanvasText, which cut the
document colour off above every overlay.
Under the hood
- The composer has 21 props instead of 27, and the formatting toolbar gets its
own tests. - The app loads 39% less JavaScript (919 kB gzip to 556 kB), because the design
system is imported per component instead of as one chunk; see WDR-0005. - The Mattermost client now handles an unexpected response body the same way in
get_thread,get_postandget_channel_posts. The first two coerced before
they checked, which made the empty-thread branch unreachable. - The pinned-trivy-binary variables are scoped to the SBOM job that uses them,
so aTRIVY_-prefixed name no longer reaches the containerised filesystem
scan, where it lands in trivy's own config namespace. - CI downloads the pinned uv and trivy binaries with
--failand three retries.
Without--failan HTTP error was written to the tarball and reported as a
checksum mismatch, so a rate-limited download looked like a tampered binary. - The test schema is now keyed on the checkout as well as the xdist worker, so
two worktrees running the suite against the same database no longer drop each
other's tables mid-run. - The synthetic dev users, the Mattermost mock, the passwordless-login routes
and the synthetic-user bootstrap moved tobackend/devtools/, which the
container image does not copy; no dev-only code ships any more. - The container build now asserts it:
import wagglesucceeds and
import devtoolsfails in the built image. - Dependency bumps: uvicorn 0.52.3, sqlalchemy 2.0.52, ruff 0.16.3, ty 0.0.72,
the CodeMirror and Lezer packages, vue-tsc 3.3.10, typescript-eslint 8.67 and
nanoid 3.3.18 (CVE-2026-67213), plus the CI images (uv 0.12.5, trivy 0.74,
renovate 44.30) and the Postgres 17 digest. Tiptap is gone as a dependency.
Container images
- Reply, new message and notes run on the design system's editor instead of
-
v2026.8.11
StableAll checks were successfulsecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 38ssecurity-scan / Python SCA (pip-audit) (push) Successful in 36srelease-and-deploy / release-scripts (push) Successful in 6srelease-and-deploy / e2e (push) Successful in 3m27stest-build / build (frontend) (pull_request) Successful in 55sCI / pre-commit (pull_request) Successful in 1m0sCI / frontend-test (pull_request) Successful in 1m0sCI / pre-commit (push) Successful in 58spublish-main / build (frontend) (push) Successful in 59srelease-and-deploy / release (push) Successful in 18stest-build / build (backend) (pull_request) Successful in 59stest-build / build (pull_request) Successful in 0sCI / backend-test (pull_request) Successful in 2m17sCI / frontend-test (push) Successful in 1m2spublish-main / build (backend) (push) Successful in 1m4spublish-main / build (push) Successful in 0srelease-and-deploy / pre-commit (push) Successful in 55sCI / e2e (pull_request) Successful in 3m29sCI / backend-test (push) Successful in 2m19sCI / release-scripts (push) Successful in 6sCI / release-scripts (pull_request) Successful in 6ssecurity-scan / SBOM (trivy) (push) Successful in 10srelease-and-deploy / build (frontend) (push) Successful in 58srelease-and-deploy / frontend-test (push) Successful in 59srelease-and-deploy / deploy (push) Successful in 2m5ssecurity-scan / SBOM (trivy) (pull_request) Successful in 10ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 14ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13ssecurity-scan / JS SCA (npm audit) (push) Successful in 15srelease-and-deploy / build (backend) (push) Successful in 1m3srelease-and-deploy / build (push) Successful in 0ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 17ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 20ssecurity-scan / Python SAST (bandit) (push) Successful in 17sCI / e2e (push) Successful in 3m30srelease-and-deploy / backend-test (push) Successful in 2m15srelease-and-deploy / ci (push) Successful in 0sreleased this
2026-08-11 21:23:00 +00:00 | 215 commits to main since this releaseChannel context around a saved message
- A saved message now offers "Toon eerdere en latere berichten", directly above
"Antwoord in thread": the messages around it in the channel, five per click in
either direction. On request you can load more. - A thread stays folded there: its root, the saved message under it when that is
a reply, and one row that opens the rest in place - "Toon 12 antwoorden", or
"Toon 11 andere antwoorden" when the saved reply is already on screen. - A saved reply now appears under its root instead of loose in the timeline.
- Beside it sits "Toon de thread": the same thread without the channel around
it. One of the two panels is open at a time. - The saved message is marked by its outline alone; the extra "bewaard" tag is
gone. - Reactions work on any message in a card's channel.
Keyboard shortcuts
- Move between screens with
gthen i/l/c/u/n/s (Inbox, Leeslijst, Concepten,
Uitgaande post, Notities, Instellingen), and through a card list withj/k
or the arrows. - Act on the focused card:
vcompletes it or marks it read,dsends it to
"Doe nu",lto Backlog,bbubbles it up,wmoves it to the other list,
rreplies andmopens it in Mattermost. - Every status change can be undone for five seconds, with
zor the button on
the notice. - Reorder a card with
Alt/Optionand an arrow key: the keyboard route to
what dragging does. - Inside an open card,
j/kstep to the next and previous one without closing
it, and the status keys move on by themselves. Cmd/Ctrl+Shift+Enteropens the schedule popover from a composer,
beside send.?lists every key. "Sneltoetsen met lettertoetsen" in Instellingen switches
the letter keys off; arrows, Tab, Enter and Esc keep working.
Instellingen
- A failed server save now puts the message on the field that caused it, instead
of in one banner under all three. - A base URL that is refused or unreachable used to report "Token is ongeldig"
on a token that was fine. Those three failures shared one message and are now
told apart. - The recovery code in the import wizard is a normal field, and a code that does
not decrypt says "De herstelcode klopt niet." at the field itself. - In "Account verwijderen", the e-mail field says why the button is disabled.
- "Thema" and "Sync-interval" are radio buttons instead of dropdowns, so every
option is visible without opening anything. "Sync-interval" is down to three
steps: "1 minuut", "5 minuten" and "1 uur". - A failed save in Voorkeuren or Automatisch opruimen can be retried with
"Opnieuw proberen". - "Opslaan..." sits beside the setting you changed, and only appears when a save
takes longer than a second. Without a connection it says "Wachten op
verbinding...".
Smaller improvements
- A channel reference now reaches Mattermost as
~town-squareinstead of
\~town-square, so it autolinks again. A doubled~~stays escaped. - A task list no longer gains a blank line between its items, so Mattermost
renders it tight. - Sending, scheduling, saving a draft or uploading an attachment used to surface
the raw server message ("card not found"). Those are Dutch sentences now. - The "Wacht op verzending" section is gone from Inbox and Leeslijst: scheduled
messages live on Uitgaande post, so they were listed twice. The red notice for
a failed send stays on both pages. - Hints that sat on the native
titleattribute are nownldd-tooltip: they
appear on keyboard focus as well as hover, and a screen reader announces them.
Where a hint only repeated the control's own label it is gone. - The send hint names the key your own system uses ("⌘ + Enter" on a Mac,
"Ctrl + Enter" elsewhere) and says nothing on a phone, where you cannot press
it. The "…" in a source path and the add-reaction button say what they do.
Fixed
- The sync-status endpoint showed every user the last sync time, status and
error message of whoever synced most recently, instead of their own: it
depended on the logged-in user but never scoped the query to them. - The scheduled-post routes accepted a draft id and acted on it: editing one
through that route replaced the draft's text and stamped a send time onto a
row nothing ever sends. Those routes now answer "niet gevonden", as they
already did for someone else's message. - A thread message whose Mattermost timestamp is missing or unreadable no longer
jumps to the top of the thread, and no longer fails the whole request. - Someone who set only a nickname in Mattermost is now shown by that nickname
everywhere; the thread view used their bare username. Cards already saved keep
the name they were stored with until the next sync.
Under the hood
- A rate-limited response now carries
Retry-AfterandX-RateLimit-*, so a
client can wait the right number of seconds and slow down before it is refused
at all. - All 22 rate-limited operations declare their 429 and name their budget; a test
reads the limits back out of the limiter, so the next one cannot arrive
undocumented. - The API reference now lives at
/api/docs, where a deployment can reach it,
renders without a CDN, and describes all 60 operations in eleven groups. - The
/apiprefix is written once, inAPI_URL_PREFIX, and applied by one
parent router instead of by each of sixteen. A route mounted outside it fails
a test. /api/healthis no longer in the OpenAPI document: it belongs to the load
balancer, not to a client.- The thread and channel-context endpoints,
/api/me/import,/api/configand
GET /api/auth/mepublish response models, so their shapes are generated
instead of hand-written on the frontend. - A card's Mattermost payload (
source_data) has a model too, with every field
optional: older cards keep loading and a missing value is filled in on read.
An import bundle is checked against it as well, so a wrong-typed payload is
refused at import instead of breaking the card list afterwards. - A Mattermost post whose
metadatais not an object no longer fails the thread
request, a non-string user id no longer fails the response model, and a
non-stringmime_typeno longer fails a sync. - Verifying a Mattermost token answers 502 when the server cannot be reached. It
was 422, the same status as an invalid token, which is why one message covered
both. - The frontend's API types are generated from the OpenAPI document
(just gen-types); CI fails when the committed file and the Pydantic schemas
disagree. Two hand-written types were wrong:CancellationReasonmissed a
value the backend sends, anduseSyncStatushad no callers. - The twelve closed value lists (card track, cleanup status, outgoing-post
state, theme, retention period and so on) each live in one place now, read by
the column, the API schema and a database CHECK alike. Ten value lists that
were declared and never read are gone. - Dialogs are now real browser dialogs. The shared modal shell is built on
nldd-window, so the backdrop, the focus trap and focus restore come from the
browser. Everything that has to float above a dialog moved with it, because
the browser top layer cannot be reached by any z-index. - The delete confirm in Instellingen moved onto that shell; the disconnect
confirm stays onnldd-modal-dialogand now registers with the overlay stack,
so the single-key shortcuts no longer stay live behind it. - Three single-key shortcuts (
c,/,?) each carried their own "is a modal
open" check that missed a component's shadow root; it is one shared check now. - Documentation is English: README, CHANGELOG,
docs/**, CONTRIBUTING, SECURITY
and the code of conduct. The interface stays Dutch.publiccode.ymlgained an
English description. - Decisions that are expensive to reverse now live in
docs/wdr/as numbered
Waggle Decision Records, starting with the NL GOV OAuth deviation, the
path-based ingress and the documentation language. docs/deployment.mdis only a runbook again; the ZAD research moved to
docs/zad-platform.md, its two go-live checklists became one, and the
instructions for one release moved to that release's CHANGELOG section.- Four documented instructions that no longer worked are corrected:
npx tsc
(which skips.vuetemplates), per-IP rate limits in SECURITY, the pre-Alembic
schema flow in the PR checklist, and a HKDF domain table missing one of five. - The frontend build was broken: markdown-it 15 imports a named
LinkifyIt
export that only linkify-it 6 has, while an npm override still forced 5.x. The
hold was meant to last only until markdown-it moved, so it is lifted. - The e2e suite drops its throwaway databases again. Each worker created one per
run and never removed it; 76 had piled up, 641MB. - The backend test job runs
pytest -n autoagain instead of a fixed six
workers, so the worker count follows the machine it lands on. - The backend runs on Python 3.14.7, with the container base image, uv and the
security-scan image raised to match. The runtime image no longer ships pip. - Dependency bumps: cryptography 50, pydantic-settings 2.15, alembic 1.19.1,
ruff 0.16.2, ty 0.0.69, markdown-it 15, jsdom 30 and NLDD 0.8.79.
Container images
- A saved message now offers "Toon eerdere en latere berichten", directly above
-
v2026.8.7
StableAll checks were successfulCI / release-scripts (pull_request) Successful in 6ssecurity-scan / SBOM (trivy) (pull_request) Successful in 9ssecurity-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15ssecurity-scan / JS SCA (npm audit) (pull_request) Successful in 16ssecurity-scan / Python SAST (bandit) (pull_request) Successful in 21sCI / pre-commit (pull_request) Successful in 28ssecurity-scan / Python SCA (pip-audit) (pull_request) Successful in 37stest-build / build (frontend) (pull_request) Successful in 55stest-build / build (backend) (pull_request) Successful in 58stest-build / build (pull_request) Successful in 0sCI / frontend-test (pull_request) Successful in 1m5sCI / backend-test (pull_request) Successful in 2m0sCI / e2e (pull_request) Successful in 2m10sCI / release-scripts (push) Successful in 6ssecurity-scan / Python SAST (bandit) (push) Successful in 20ssecurity-scan / SBOM (trivy) (push) Successful in 11ssecurity-scan / Filesystem scan (trivy fs) (push) Successful in 14ssecurity-scan / JS SCA (npm audit) (push) Successful in 15sCI / pre-commit (push) Successful in 27ssecurity-scan / Python SCA (pip-audit) (push) Successful in 38spublish-main / build (frontend) (push) Successful in 58spublish-main / build (backend) (push) Successful in 1m2spublish-main / build (push) Successful in 0sCI / frontend-test (push) Successful in 1m4sCI / backend-test (push) Successful in 1m58sCI / e2e (push) Successful in 2m9srelease-and-deploy / release-scripts (push) Successful in 5srelease-and-deploy / pre-commit (push) Successful in 22srelease-and-deploy / release (push) Successful in 28srelease-and-deploy / build (frontend) (push) Successful in 56srelease-and-deploy / frontend-test (push) Successful in 59srelease-and-deploy / build (backend) (push) Successful in 1m2srelease-and-deploy / build (push) Successful in 0srelease-and-deploy / backend-test (push) Successful in 1m55srelease-and-deploy / e2e (push) Successful in 2m5srelease-and-deploy / ci (push) Successful in 0srelease-and-deploy / deploy (push) Successful in 1m54sreleased this
2026-08-07 19:27:37 +00:00 | 289 commits to main since this releaseLet op bij het uitrollen
- Deze release vraagt een stop-then-start-deploy, geen rolling update. De
vorige en de nieuwe versie kunnen niet tegelijk op dezelfde database: zolang
ze naast elkaar draaien, krijgt elke gebruiker foutmeldingen van de oude
versie. - Draai de twee controlequery's uit
docs/deployment.mdvooraf (identifiers en
cleanup_undo_state). Vindt een van beide iets, ruim dat op vóór de deploy -
de migratie stopt er anders op. - Een export uit een oudere versie kan geweigerd worden bij het
terugzetten. Een bundel die op een SQLite-installatie is gemaakt bevat
tijdstippen zonder tijdzone; de import weigert die nu met
Input should have timezone info(HTTP 400).docs/deployment.mdbeschrijft
hoe je zo'n bundel repareert: de tijdstippen in de JSON eenZgeven.
Nieuw bericht opstellen
- Je kunt nu nieuwe berichten opstellen en laten afleveren in een kanaal of als
direct bericht bij een persoon: kies een doel, schrijf je bericht met opmaak,
@-mentions en bijlagen, en verstuur het direct of plan het in. Openen via
"Nieuw bericht" in het menu of met de sneltoetsc. - Een bericht dat je nog niet wilt versturen bewaar je als concept. Je kunt aan
meerdere concepten tegelijk werken; ze staan onder Concepten, waar je ze weer
opent. Ook een mislukt gepland bericht pak je daar op om het opnieuw te plannen. - Een concept gooi je weg op de Concepten-pagina, of door het helemaal leeg te
maken - dat laatste geldt ook voor een antwoord in een thread. Je hebt tien
seconden om dat ongedaan te maken; daarna verdwijnt het concept met zijn
bijlagen. - Je kunt nu ook een bijlage meesturen met een concept of gepland bericht;
voorheen kon dat alleen bij een direct antwoord. - In alle composers toont het kopjesmenu per niveau een icoon, zodat je Kop 1
tot en met Kop 6 sneller uit elkaar houdt.
Overzichtspagina's
- Nieuwe pagina's Concepten, Uitgaande post en Notities (via het menu)
verzamelen je concepten, geplande en verzonden berichten, en notities op één
plek.
Automatisch opruimen
- Afgehandelde berichten en voltooide of gelezen kaarten worden nu desgewenst
vanzelf opgeruimd (standaard na 90 dagen; instelbaar op uit, 30, 90, 180 of
365). Concepten en kaarten met een notitie blijven altijd staan. - Bij bestaande accounts staat deze optie standaard uit. Deze accounts krijgen
een eenmalig bericht dat aanbiedt om het op 90 dagen te zetten. - Bijlagen van geannuleerde en mislukte berichten bleven onbeperkt op het volume
staan. Dat lossen we ook direct op.
Plannen en uitstellen
- Bij "Zelf kiezen…" kies je datum en tijd nu met een echte kalender (met
weeknummers) en een keuzewiel ernaast, in plaats van met twee kale
invoervelden. - De plan- en snooze-popover zijn nu met het toetsenbord te bereiken en te
bedienen; het waren muis-only dialogen die buiten de focus-trap vielen. De
focus gaat er bij openen in en keert bij sluiten terug naar de knop, je loopt
met de pijltjestoetsen door de opties, en Escape sluit alleen de popover, niet
de kaart eronder (WCAG 2.1.1). - De API weigert nu een tijdstip zonder tijdzone, in plaats van er UTC van te
maken. Een gepland bericht dat om09:00werd ingestuurd zonder offset
vertrok daardoor een tot twee uur te laat, afhankelijk van zomer- of
wintertijd. De app zelf stuurde altijd al een tijdzone mee; dit raakt eigen
scripts, handmatig bewerkte import-bestanden en exports uit een oudere versie
(zie "Let op bij het uitrollen").
Voorwaarden en welkom
- Je ziet nu op de voorwaardenpagina wanneer je de voorwaarden accepteerde, en
kunt ze altijd teruglezen via het menu-item Voorwaarden. - De welkomstpagina stopt na het accepteren van de voorwaarden; Mattermost
koppelen doe je in Instellingen (een banner wijst je daarheen als er nog geen
server gekoppeld is).
Toegankelijkheid
- Nieuwe link "Direct naar de inhoud" bovenaan elke pagina: druk op Tab en je
slaat het menu over. Ook een schermlezer springt nu direct naar de inhoud. - Focus blijft binnen een geopende modal: de focus-trap zag knoppen in
web-components en de tekst-editor (contenteditable) eerder over het hoofd,
waardoor Tab naar de achtergrond kon weglopen (WCAG 2.4.3). - De sneltoets
/opent de zoekbalk niet meer terwijl je typt in een invoerveld
binnen een web-component (zoals het Mattermost-token-veld), en ook niet meer
achter een geopende kaart (WCAG 2.1.4). - Een reactie op een bericht vertelt nu ook aan een schermlezer of jij hem
zelf hebt gegeven; dat was alleen aan de kleur te zien. - De kaartdetail-modal markeert het paneel zelf als dialoog in plaats van de
achtergrondlaag, zodat een schermlezer de juiste grens aanhoudt. - Instellingen gebruikt nu overal dezelfde opbouw voor een veld: label, hint
en foutmelding komen uit NLDD. Hints worden daardoor ook echt voorgelezen;
ze waren eerder alleen zichtbaar. - Een mislukte opslag bij Voorkeuren was volledig stil en is nu zichtbaar.
Een thema dat niet opgeslagen kon worden draait ook weer terug. - De vinkjes in de import-tabel heten nu hetzelfde als hun zichtbare label
("Toepassen: Thema" in plaats van "Toepassen: theme"), zodat je ze met
spraakbediening kunt aanwijzen. - Bij "Account verwijderen" springt de focus nu echt naar het eerste vinkje;
dat was de bedoeling maar gebeurde niet. Het e-mailveld doet geen
spellingscontrole meer.
Tekst en vormgeving
- Tekst is groter, gelijkmatiger en staat ruimer op de regel. Berichten,
uitlegteksten, formulierlabels en knoppen stonden verspreid op 10 tot 16px
terwijl de pagina op 18px staat; ze volgen nu de basis, met een regelafstand
van 1.5 in plaats van 1.125. Compacte menu's houden bewust 16px aan, het
formaat dat NLDD zelf voor dat soort oppervlakken gebruikt. - Waggle heeft veel minder eigen ontwerp-tokens: lettergroottes, spacing,
hoekafronding, letterdiktes, schaduwen, focus-ring en overgangen komen nu uit
NLDD in plaats van uit een eigen reeks ernaast. Vetgedrukte tekst wordt
daardoor iets minder zwaar (600 in plaats van 700) en de kleinste iconen
worden 16px (waren 12-14px). - Hoeken en kaders zijn overal gelijk: kaarten, vensters, menu's en zwevende
panelen (plannen, bron-pad, "Aan") zijn even rond en krijgen alleen schaduw
en hoekstraal, geen extra haarlijn. Sommige menu's en popovers waren nog
vierkant. - Knoppen, lege toestanden, labels en initialen-cirkels komen nu uit NLDD in
plaats van uit eigen CSS. De knopjes op kaarten, in het kaartdetail en op de
rijen van uitgaande post zijn daardoor overal even groot, reageren hetzelfde
op de muis en zijn met Tab allemaal bereikbaar; een kapotte profielfoto valt
vanzelf terug op de initialen. Daarmee staat er nergens in de app nog een
hardgecodeerde lettergrootte. - Een paar iconen dragen nu het begrip dat ze bedoelen: een video-bijlage toonde
een leeg vakje en krijgt een afspeel-driehoek, "Verzend nu" een pijl naar het
eind in plaats van een terugdraaiende klok, en Backlog een eigen icoon zodat
het niet meer hetzelfde oogt als Inbox. - NLDD-design-system naar 0.8.78: kaarten hebben nu een rand in plaats van een
schaduw, en foutmeldingen en scheidingslijnen in menu's krijgen meer ruimte.
Opgelost
- Waggle gebruikt RijksSans niet meer. Dat font is volgens de voorwaarden van de
Rijksoverheid voorbehouden aan publicaties van de Rijksoverheid en aan werk in
haar opdracht; Waggle valt daarbuiten. De app gebruikt nu het systeemlettertype
en levert de fontbestanden ook niet meer mee. JetBrains Mono (vrij
gelicentieerd) blijft voor code. - Foutmeldingen zijn weer rood. Ze verwezen naar een kleur-token dat niet
bestaat, waardoor 19 meldingen (banners, toasts, formulierfouten, mislukte
bijlagen) in gewone tekstkleur stonden en de rode banner-achtergrond helemaal
wegviel. - De opmaak van Mattermost-berichten staat nu op één plek. Zes componenten
hadden er elk een eigen kopie van, die onderling uit elkaar waren gelopen:
koppen ontbraken op de inbox-kaart, en de spatiëring verschilde per scherm. - De muiscursor volgt nu de NLDD-conventie: knoppen tonen de standaardpijl; het
handje is voor links en klikbare kaarten/rijen. Voorheen was dit door elkaar
(de ene knop toonde wel een handje, de andere niet).
Onder de motorkap
- Waggle draait nu alleen nog op Postgres; de SQLite-optie is vervallen. Dit
maakt tijdzone-bewuste datums en database-validatie mogelijk. Een andere
database-URL danpostgresql+asyncpg://wordt bij het starten geweigerd, en
een URL uitbackend/.envwerkt weer. - Identifiers zijn nu echte UUID's in plaats van tekst die er zo uitziet: de
database controleert ze, vergelijkt ze sneller en slaat ze half zo groot op.
De ongedaan-maken-momentopname van een kaart was tekst met JSON erin en is nu
een echte JSON-kolom. - Negen ongebruikte Python-pakketten uit het backend-image gehaald (50 -> 41).
Ze telden wel mee in de kwetsbaarhedenscan, die een uitrol kan blokkeren op
code die nooit draait. De beheercommando'swaggle generate-secretsen
waggle rotate-keyswerken ongewijzigd. - De backend-container start sneller: de Python-bytecode zit nu in het image in
plaats van dat elke start hem opnieuw wegschrijft onder/app(1,60 s -> 0,68 s
voor het inladen van de app; het image wordt er 34 MB groter van). - Een bijlage die je uploadt staat nu ook onderweg op het gegevensvolume in
plaats van op de schijf van de pod. Die schijf kon vollopen buiten het zicht
van de ruimtecontrole om, met een pod-herstart als gevolg; een volle schijf
geeft nu de nette melding in plaats van een serverfout. - Geparkeerde bijlagen worden nu versleuteld op schijf (eigen sleutel-domein),
zodat een volume-snapshot of backup de inhoud niet prijsgeeft. - Nieuwe dagelijkse
retention_sweep-loop past de bewaartermijnen per gebruiker toe
en legt alleen aantallen per categorie vast in het audit-log (retention_purged). - Logging is samengevoegd tot één centrale pijplijn (structlog
ProcessorFormatterop de root-handler): structlog-, stdlib- én
uvicorn-logs gaan door dezelfde redactie en dezelfde rendering (JSON in
productie). Voorheen omzeilden structlog en uvicorn de redactie allebei. - Redactiepatronen verbreed: ook geciteerde/JSON-vormen (
"token": "..."),
alle cookie-pairs, OAuth-queryparameters (?code=/&state=),
secret-veldnamen en traceback-tekst worden geredact. - Logniveau instelbaar via
WAGGLE_LOG_LEVEL(default INFO); stdlib-INFO
(o.a. CSRF-blocks, audit-events) verscheen voorheen helemaal niet.
Elke request krijgt eenrequest_idin alle bijbehorende logregels. - Alembic zet de logging-config niet meer terug bij het opstarten: de
in-process migratie sloopte de redactie-handler en dempte de
uvicorn-loggers op elke boot (fileConfigdraait nu alleen nog voor de
losse Alembic-CLI). - De build en de tests slaan nu alarm bij fouten die eerder geruisloos
doorgingen: een verdwenen ontwerp-token, een meegeleverd RijksSans-font, een
icoonnaam die niet meer bestaat, een ongeldige waarde op een NLDD-component
(variant, kleur, formaat) en een NLDD-component die zijn padding weer op de
buitenkant zet. - CI is sneller: de e2e-job bouwde de frontend twee keer en typecheckte opnieuw
wat de frontend-job al deed, en draait nu op drie parallelle workers met een
eigen backend en database per worker (lokaal 76s -> 25s). De backend-suite
draait op een vast aantal workers in plaats van-n auto, dat de cores van de
hele host claimde en naast een tweede CI-run tot tienvoudige looptijden leidde. - Renovate bumpt npm- en Python-dependencies nu ook binnen een bestaande range
(rangeStrategy: bump). De defaultreplaceliet>=-floors en
caret-ranges op hun oude versie staan, waardoor routine-updates (inclusief
in-range security-patches) stil bleven liggen. - Dependencies bijgewerkt: fastapi 0.141.1, uvicorn 0.52.1, sqlalchemy 2.0.51,
alembic 1.19.0, pydantic 2.13.4, structlog 26.1, tiptap 3.29.2, dompurify
3.4.13, vite 8.2.0, typescript-eslint 8.66 en cryptography 50.0.0
(security-advisory). - CI-toolchain bijgewerkt: uv 0.12.1, trivy 0.73.0, node 24.19.0-alpine en een
nieuw nginx-basisimage; de fail-closed sha256-pins voor uv en trivy zijn
tegen de upstream-checksums gecontroleerd. - Drie ongebruikte toegankelijkheidspakketten verwijderd; dat scheelt 134
pakketten en vier van de zes high-severity meldingen innpm audit. - Alle vensters (opstellen, kaartdetail, herstelcode, sneltoetsen) delen nu één
Modal-component: overlay, focus-trap, scroll-lock, Escape, header en breedte
zitten op één plek in plaats van in vier eigen kopieën.
Container images
- Deze release vraagt een stop-then-start-deploy, geen rolling update. De