• v2026.8.11 7d7c8f76fa

    v2026.8.11
    All checks were successful
    security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
    security-scan / Python SCA (pip-audit) (push) Successful in 36s
    release-and-deploy / release-scripts (push) Successful in 6s
    release-and-deploy / e2e (push) Successful in 3m27s
    test-build / build (frontend) (pull_request) Successful in 55s
    CI / pre-commit (pull_request) Successful in 1m0s
    CI / frontend-test (pull_request) Successful in 1m0s
    CI / pre-commit (push) Successful in 58s
    publish-main / build (frontend) (push) Successful in 59s
    release-and-deploy / release (push) Successful in 18s
    test-build / build (backend) (pull_request) Successful in 59s
    test-build / build (pull_request) Successful in 0s
    CI / backend-test (pull_request) Successful in 2m17s
    CI / frontend-test (push) Successful in 1m2s
    publish-main / build (backend) (push) Successful in 1m4s
    publish-main / build (push) Successful in 0s
    release-and-deploy / pre-commit (push) Successful in 55s
    CI / e2e (pull_request) Successful in 3m29s
    CI / backend-test (push) Successful in 2m19s
    CI / release-scripts (push) Successful in 6s
    CI / release-scripts (pull_request) Successful in 6s
    security-scan / SBOM (trivy) (push) Successful in 10s
    release-and-deploy / build (frontend) (push) Successful in 58s
    release-and-deploy / frontend-test (push) Successful in 59s
    release-and-deploy / deploy (push) Successful in 2m5s
    security-scan / SBOM (trivy) (pull_request) Successful in 10s
    security-scan / Filesystem scan (trivy fs) (push) Successful in 14s
    security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
    security-scan / JS SCA (npm audit) (push) Successful in 15s
    release-and-deploy / build (backend) (push) Successful in 1m3s
    release-and-deploy / build (push) Successful in 0s
    security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
    security-scan / Python SAST (bandit) (pull_request) Successful in 20s
    security-scan / Python SAST (bandit) (push) Successful in 17s
    CI / e2e (push) Successful in 3m30s
    release-and-deploy / backend-test (push) Successful in 2m15s
    release-and-deploy / ci (push) Successful in 0s
    Stable

    robbertbos released this 2026-08-11 21:23:00 +00:00 | 55 commits to main since this release

    Channel 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 g then i/l/c/u/n/s (Inbox, Leeslijst, Concepten,
      Uitgaande post, Notities, Instellingen), and through a card list with j/k
      or the arrows.
    • Act on the focused card: v completes it or marks it read, d sends it to
      "Doe nu", l to Backlog, b bubbles it up, w moves it to the other list,
      r replies and m opens it in Mattermost.
    • Every status change can be undone for five seconds, with z or the button on
      the notice.
    • Reorder a card with Alt/Option and an arrow key: the keyboard route to
      what dragging does.
    • Inside an open card, j/k step to the next and previous one without closing
      it, and the status keys move on by themselves.
    • Cmd/Ctrl+Shift+Enter opens 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-square instead 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 title attribute are now nldd-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-After and X-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 /api prefix is written once, in API_URL_PREFIX, and applied by one
      parent router instead of by each of sixteen. A route mounted outside it fails
      a test.
    • /api/health is 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/config and
      GET /api/auth/me publish 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 metadata is not an object no longer fails the thread
      request, a non-string user id no longer fails the response model, and a
      non-string mime_type no 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: CancellationReason missed a
      value the backend sends, and useSyncStatus had 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 on nldd-modal-dialog and 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.yml gained 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.md is 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 .vue templates), 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 auto again 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