New-message composer: overlay, shared editor, mentions and queue #192

Merged
robbertbos merged 1 commit from fase-5-composer into main 2026-08-05 13:37:11 +00:00
Owner

Compose a new Mattermost message to a channel or person, independent of a saved
card, and send it now (with a pre-send undo window) or schedule it - consuming
the existing outgoing-message backend.

Composer

  • ComposeOverlay: a centered Modal (shared components/Modal.vue: focus-trap
    • scroll-lock + Escape + backdrop) with the full editor stack, TargetPicker and
      a send-now / schedule split-button. Content and target autosave to a concept; you can work on several concepts
      at once. During the undo window the pending send locks its concept (hidden
      from Concepten, not reopenable); the concept is deleted only after Mattermost
      accepted the message, so a failed send or a crash never loses composed text
      or attachments.
  • TargetPicker: a WCAG combobox over the account's channels/conversations plus
    a debounced people search to start a DM. The search matches every typed token
    against the channel name and its team name ("off topic groep x").
  • Entry point: a "Nieuw bericht" top-nav item + a c shortcut, behind a
    device-local single-key-shortcuts preference (WCAG 2.1.4).

Shared editor + mentions

  • MessageEditor bundles the MarkdownEditor + formatting toolbar + link bar +
    emoji picker + optional @-mention autocomplete, used by both the reply
    composer and the new-message composer so they cannot drift.
  • Mentions are scoped by context: a card's channel for a reply, the chosen
    target channel for a new message (new GET /api/mm/mention-candidates, sharing
    the reply endpoint's member/autocomplete logic).

Backend

  • Card-less write endpoints (POST /api/messages, /api/scheduled-posts) and per-user
    new-message drafts (multiple concepts per user). The composer-facing API is renamed from "cardless"
    to "new message"; the data-layer term (a row with card_id IS NULL)
    deliberately stays "card-less".
  • A single MAX_FILE_REFS for the attachment cap. A dev Mattermost mock behind a
    dedicated dev_mm_mock flag makes the composer testable locally without a real
    server, committed with full coverage.

Housekeeping

  • Shared AttachmentChipList (image thumbnails everywhere) and a
    useMessageComposer attachment core remove duplication between the two
    composers.
  • Rebased onto main and migrated to NLDD's primitive tokens.

Undo is a pre-send delay toast (lib/delayedSend), not a post-send delete.

Review round

A deep multi-agent review of everything since v2026.7.24 (with adversarial
verification per finding) confirmed 28 issues; all are fixed in the six
commits from c512a6a to 77a8839. Highlights:

  • Critical: sending a new message with attachments always failed and
    destroyed the concept (draft delete reaped the parked files before the
    delayed send fired). The concept now lives until the send succeeded.
  • Critical: merge-importing a backup duplicated a still-pending scheduled
    post (dedupe keyed on an idempotency key that exports never carry) - the
    message fired twice. Dedupe now uses content-shaped keys.
  • Duplicate-send windows closed: ambiguous transport errors fail closed
    instead of retrying outside Mattermost's 30s dedupe cache, the runner claim
    is an atomic state-guarded UPDATE, replies carry a pending_post_id, and the
    composers reuse one dedupe key across retries.
  • Export/import round-trips DM and target-less concepts and multiple drafts.
  • Recovery path: a failed/cancelled card-less scheduled message can be
    restored as a concept, including server, target and attachments.
  • Plus: UTC normalization for scheduled_at, dev_mm_mock production veto,
    deterministic migration downgrade, silent-autosave feedback, a11y fixes
    (Server select name, error-text contrast) and authorization tests for
    mention-candidates.
Compose a new Mattermost message to a channel or person, independent of a saved card, and send it now (with a pre-send undo window) or schedule it - consuming the existing outgoing-message backend. ## Composer - **ComposeOverlay**: a centered Modal (shared `components/Modal.vue`: focus-trap + scroll-lock + Escape + backdrop) with the full editor stack, TargetPicker and a send-now / schedule split-button. Content and target autosave to a concept; you can work on several concepts at once. During the undo window the pending send locks its concept (hidden from Concepten, not reopenable); the concept is deleted only after Mattermost accepted the message, so a failed send or a crash never loses composed text or attachments. - **TargetPicker**: a WCAG combobox over the account's channels/conversations plus a debounced people search to start a DM. The search matches every typed token against the channel name *and* its team name ("off topic groep x"). - **Entry point**: a "Nieuw bericht" top-nav item + a `c` shortcut, behind a device-local single-key-shortcuts preference (WCAG 2.1.4). ## Shared editor + mentions - **MessageEditor** bundles the MarkdownEditor + formatting toolbar + link bar + emoji picker + optional @-mention autocomplete, used by *both* the reply composer and the new-message composer so they cannot drift. - **Mentions** are scoped by context: a card's channel for a reply, the chosen target channel for a new message (new `GET /api/mm/mention-candidates`, sharing the reply endpoint's member/autocomplete logic). ## Backend - Card-less write endpoints (`POST /api/messages`, `/api/scheduled-posts`) and per-user new-message drafts (multiple concepts per user). The composer-facing API is renamed from "cardless" to "new message"; the data-layer term (a row with `card_id IS NULL`) deliberately stays "card-less". - A single `MAX_FILE_REFS` for the attachment cap. A dev Mattermost mock behind a dedicated `dev_mm_mock` flag makes the composer testable locally without a real server, committed with full coverage. ## Housekeeping - Shared `AttachmentChipList` (image thumbnails everywhere) and a `useMessageComposer` attachment core remove duplication between the two composers. - Rebased onto main and migrated to NLDD's primitive tokens. Undo is a pre-send delay toast (`lib/delayedSend`), not a post-send delete. ## Review round A deep multi-agent review of everything since v2026.7.24 (with adversarial verification per finding) confirmed 28 issues; all are fixed in the six commits from `c512a6a` to `77a8839`. Highlights: - **Critical:** sending a new message with attachments always failed and destroyed the concept (draft delete reaped the parked files before the delayed send fired). The concept now lives until the send succeeded. - **Critical:** merge-importing a backup duplicated a still-pending scheduled post (dedupe keyed on an idempotency key that exports never carry) - the message fired twice. Dedupe now uses content-shaped keys. - Duplicate-send windows closed: ambiguous transport errors fail closed instead of retrying outside Mattermost's 30s dedupe cache, the runner claim is an atomic state-guarded UPDATE, replies carry a pending_post_id, and the composers reuse one dedupe key across retries. - Export/import round-trips DM and target-less concepts and multiple drafts. - Recovery path: a failed/cancelled card-less scheduled message can be restored as a concept, including server, target and attachments. - Plus: UTC normalization for scheduled_at, dev_mm_mock production veto, deterministic migration downgrade, silent-autosave feedback, a11y fixes (Server select name, error-text contrast) and authorization tests for mention-candidates.
robbertbos force-pushed fase-5-composer from a4db9cb918
All checks were successful
CI / pre-commit (pull_request) Successful in 1m23s
CI / frontend-test (pull_request) Successful in 5m19s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m1s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 8m12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 23s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
CI / backend-test-postgres (pull_request) Successful in 10m10s
test-build / build (backend) (pull_request) Successful in 1m55s
test-build / build (frontend) (pull_request) Successful in 2m14s
CI / e2e (pull_request) Successful in 9m21s
test-build / build (pull_request) Successful in 0s
to 1a0e760f87
All checks were successful
CI / release-scripts (pull_request) Successful in 25s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 32s
security-scan / SBOM (trivy) (pull_request) Successful in 32s
security-scan / JS SCA (npm audit) (pull_request) Successful in 37s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
test-build / build (backend) (pull_request) Successful in 1m18s
test-build / build (frontend) (pull_request) Successful in 1m22s
CI / backend-test (pull_request) Successful in 1m23s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m33s
CI / pre-commit (pull_request) Successful in 1m38s
CI / e2e (pull_request) Successful in 4m22s
CI / backend-test-postgres (pull_request) Successful in 4m53s
2026-07-26 06:22:30 +00:00
Compare
The composer preview needs a fake Mattermost (fixed channels/people and
pretend sends) so it works locally without a real server. The mock lived only
in the working tree, gated on dev_synthetic_auth - but that flag is forced on
across the whole test suite, so committing it as-is shadowed the real client in
every test that resolves an MM client (17 failures).

Add a dedicated dev_mm_mock setting (off by default, unset in tests) and gate
DevMockMattermostClient on it in both get_mm_client_for_card and the scheduled
runner's per-row client build. Commit the mock with full unit coverage plus a
gate-on branch test for each call site, keeping the 100% backend coverage gate
green.
The "max 5 files per outgoing message" cap was duplicated: seven Pydantic
Field(max_length=5) sites plus a raw guard in the Mattermost client on the
backend, and a local `const MAX_FILES = 5` in each composer on the frontend.

Introduce MAX_FILE_REFS (backend, in schemas/outgoing_post.py) and
MAX_ATTACHMENTS (frontend, in attachmentLimits.ts) and route every site through
them. The two constants cannot share a runtime value across the wire, so each
carries a comment pointing at the other; changing the cap is now one edit per
side.
The composer speaks the product term (a "new message"), so its API, hook, prop
and component identifiers should too:

- backend: CardlessScheduledCreate -> NewMessageScheduledCreate,
  CardlessMessageCreate -> NewMessageCreate, service create_scheduled_cardless
  -> create_scheduled_new_message, endpoint create_scheduled_post_cardless ->
  create_new_message_scheduled_post; test files renamed to match.
- frontend: CreateCardlessScheduledArgs -> CreateNewMessageScheduledArgs,
  useCreateCardlessScheduled -> useCreateNewMessageScheduled, the SendButton /
  SchedulePopover `cardless` prop -> `newMessage`, QueuedRow's isCardless ->
  isNewMessage.

Deliberately kept: "card-less" (card_id IS NULL) as the data-layer term in the
model, migration, import_service dedup helpers and their tests. That names the
DB condition precisely; "new message" is the UX concept. No wire format, URL,
DB column or migration changes - only OpenAPI component titles.
ReplyComposer and ComposeOverlay each rendered the pending-upload list
themselves, and the two had drifted: ComposeOverlay's copy showed only a
filename and text status, dropping the image thumbnails and spinner
ReplyComposer had. Pull the richer version into
components/attachments/AttachmentChipList.vue (files in, remove/retry out) and
use it in both, so a picked image previews the same way wherever it is composed.
ReplyComposer keeps its own separate sent-view chips.
ReplyComposer and ComposeOverlay each carried their own copy of the same
attachment plumbing: doneRefs / uploadingCount / erroredFiles, the aria-live
attachmentStatus, and the MAX_ATTACHMENTS cap logic. Move that into
composables/useMessageComposer (built on useFileUpload) and consume it from
both.

Deliberately out of scope: send/schedule/undo stays per-caller. A reply runs a
state machine with a sent-view; a new message owns its draft across the undo
window and closes optimistically. Both already share the scheduleDelayedSend
primitive, so folding their orchestration into one injected strategy would be a
leaky abstraction over the production reply path for no real gain.
ComposeOverlay used a native <dialog>, which rendered top-aligned: the global
`* { margin: 0 }` reset overrides the UA `dialog:modal { margin: auto }` that
would have centered it. Rather than patch that one dialog, extract a Modal shell
(components/Modal.vue) mirroring the app's other modals - a centered backdrop
panel with useFocusTrap, useScrollLock, Escape-to-close and backdrop-click - and
render ComposeOverlay through it. Focus now traps inside the panel and restores
to the invoker on close.
The compose editor only had the bare MarkdownEditor. Wire in the full editor
stack (FormatToolbar + LinkBar + EmojiPicker), same as ReplyComposer: capture
the editor via :on-ready into a shallowRef and give the toolbar a
container:composer context so its progressive-overflow works in the 40rem
dialog. The attach affordance moves from an inline "Bijlage" button to the
toolbar's paperclip, driving a hidden file input.

NLDD alignment: the server <select> becomes nldd-form-field + nldd-dropdown and
the close button an nldd-icon-button, deleting the custom .select/.closeBtn CSS.
EmojiPicker's Escape becomes element-scoped + stopPropagation (like LinkBar) so
it closes only the picker, not the enclosing Modal.
The composer's target list resolves DM channels' member names via
get_users_by_ids, which the mock did not override - so its seeded DM channel
fell through to the base client and hit the unroutable dev-mock host, 502-ing
the whole target lookup. Override it to return the fake people (unknown ids
omitted, like the real client), so channel targets load in the preview.
The combobox's magnifier sat over the start of the placeholder: a global
`input[type="text"] { padding }` rule (specificity 0,1,1) beat the bare `.input`
class (0,1,0), so the left inset meant to clear the icon never applied. Scope the
rule to `.comboWrap .input` (0,2,0) to win the cascade, and set the left padding
to clear the icon plus a gap. The combobox stays custom - moving it to
nldd-search-field would jeopardise its WCAG combobox ARIA contract.
Changelog: composer centering, toolbar and search-icon fix
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / JS SCA (npm audit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 56s
test-build / build (backend) (pull_request) Successful in 1m1s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m21s
CI / e2e (pull_request) Successful in 4m7s
CI / frontend-test (pull_request) Successful in 4m16s
CI / backend-test (pull_request) Successful in 5m38s
CI / backend-test-postgres (pull_request) Successful in 9m24s
8d13322290
Add the user-visible refinements to the "Nieuw bericht opstellen" section (the
composer now centers and carries the full formatting toolbar; attachments show
image thumbnails) and note the fixed "Aan"-field icon overlap under Opgelost.
The target filter matched the query as one literal substring against the channel
name only, so "off topic groep x" found nothing. Split the query into tokens and
require each to appear in the channel name OR its team name, so you can narrow to
a channel by its group the way Mattermost does.
The reply composer resolves @-mentions against a saved card's channel; the
new-message composer has no card. Add GET /api/mm/mention-candidates
(account_id + channel_id) that reuses the same list_channel_members /
autocomplete_users logic, extracted into a shared _mention_candidates helper so
the card and channel endpoints do not drift. Channel visibility is enforced by
Mattermost against the caller's own token. The dev mock gains autocomplete_users
+ list_channel_members so mentions work in the preview.
Extract MessageEditor: MarkdownEditor + formatting toolbar + link bar + emoji
picker + optional @-mention autocomplete, wired once and used by both the reply
composer and the new-message composer (which had no mentions at all). Mentions
are scoped by the caller through mentionFetch - a card's channel for a reply, the
chosen target channel for a new message (fetchChannelMentionCandidates + the new
/api/mm/mention-candidates endpoint). The reply composer keeps its exact layout;
the new-message composer now matches it. Ctrl/Cmd+Enter sends from either.
Merge origin/main; align the composer with NLDD's tokens
Some checks failed
CI / pre-commit (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
CI / backend-test-postgres (pull_request) Has been cancelled
CI / frontend-test (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / release-scripts (pull_request) Has been cancelled
security-scan / Python SCA (pip-audit) (pull_request) Has been cancelled
security-scan / Python SAST (bandit) (pull_request) Has been cancelled
security-scan / JS SCA (npm audit) (pull_request) Has been cancelled
security-scan / Filesystem scan (trivy fs) (pull_request) Has been cancelled
security-scan / SBOM (trivy) (pull_request) Has been cancelled
test-build / build (pull_request) Has been cancelled
test-build / build (backend) (pull_request) Has been cancelled
test-build / build (frontend) (pull_request) Has been cancelled
97e9027503
Main retired Waggle's spacing/radius/type/weight/shadow tokens in favour of
NLDD's primitives. Resolve the conflicts to main's versions (numeric nldd-icon
sizes, the schedule-checkbox layout), keep the composer refactor (shared
MessageEditor, mentions, AttachmentChipList), and migrate every retired token in
the six composer files to its NLDD equivalent: --space-N -> --primitives-space-4N,
--radius-{sm,md,lg} -> --primitives-corner-radius-{xs,sm,md}, --text-* ->
--primitives-font-size-*, --shadow-lg -> --semantics-overlays-box-shadow,
--transition-fast -> --primitives-transition-duration-fast, and
--semantics-content-error-color -> --semantics-content-critical-color.
robbertbos changed title from Card-less composer: new-message overlay + queue integration to New-message composer: overlay, shared editor, mentions and queue 2026-07-26 10:11:02 +00:00
Add a keyboard-shortcut help overlay on "?"
All checks were successful
CI / pre-commit (pull_request) Successful in 1m23s
CI / frontend-test (pull_request) Successful in 5m56s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 56s
security-scan / Python SAST (bandit) (pull_request) Successful in 36s
CI / backend-test (pull_request) Successful in 8m16s
security-scan / JS SCA (npm audit) (pull_request) Successful in 39s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 25s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test-postgres (pull_request) Successful in 9m48s
test-build / build (backend) (pull_request) Successful in 1m50s
CI / e2e (pull_request) Successful in 9m25s
test-build / build (frontend) (pull_request) Successful in 2m12s
test-build / build (pull_request) Successful in 0s
e77dae7ef7
A centered Modal listing the app-global shortcuts (/ search, c compose, ?) and
the composer editor shortcuts (Ctrl/Cmd+Enter send, Ctrl/Cmd+K link). Opened with
? behind the single-key-shortcuts preference (WCAG 2.1.4), never while typing in a
field or behind another modal. The changelog also records the new-message
mentions and the smarter "Aan" search.
The mock's channel/user ids ("c-town", "u-sam") were not the 26-char lowercase-
alphanumeric shape real Mattermost uses, and mm_targets_service.resolve_target
enforces that shape at submit-time - so scheduling or sending a new message in
the preview failed with "ongeldig kanaal-id", while the read paths (listing
channels, mentions) worked. Derive valid ids from a short seed via _mmid().
Align the compose modal with the reply composer
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16s
security-scan / JS SCA (npm audit) (pull_request) Successful in 24s
security-scan / Python SAST (bandit) (pull_request) Successful in 27s
CI / pre-commit (pull_request) Successful in 33s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m1s
CI / backend-test (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m5s
test-build / build (frontend) (pull_request) Successful in 1m14s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m28s
CI / e2e (pull_request) Successful in 4m4s
CI / backend-test-postgres (pull_request) Successful in 4m18s
e121452107
The new-message modal read as loose form fields: the message area had no
visible frame or focus highlight, the "Aan" field collapsed to a bare pill once
a target was picked, and the narrow 40rem panel clipped the formatting toolbar.

- Widen the panel to 52rem so the full toolbar fits and the editor breathes.
- Give "Nieuw bericht" a proper nldd-title header instead of a raw <h2>.
- Frame the message editor like ReplyComposer's .wrapper: one bordered box with
  a focus-within highlight, a background and a taller default min-height.
- Turn the selected "Aan" target into a full-width bordered field instead of a
  pill, matching the Server dropdown (also drops the retired --radius-full).
Center the target-picker listbox status text
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
CI / pre-commit (pull_request) Successful in 29s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 1m1s
test-build / build (frontend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m12s
CI / e2e (pull_request) Successful in 3m6s
CI / backend-test-postgres (pull_request) Successful in 3m56s
977e080412
The "Bezig met laden...", "Niets gevonden" and "Typ om te zoeken" placeholder
lines were left-aligned and read as a stray list row; center them so they land
as tidy status text under the input.
Fix the invisible "Aan" field border (NLDD token misuse)
All checks were successful
CI / pre-commit (pull_request) Successful in 40s
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 15s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 22s
security-scan / JS SCA (npm audit) (pull_request) Successful in 29s
security-scan / Python SAST (bandit) (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 49s
CI / backend-test (pull_request) Successful in 1m4s
CI / frontend-test (pull_request) Successful in 1m23s
test-build / build (backend) (pull_request) Successful in 1m47s
test-build / build (frontend) (pull_request) Successful in 1m48s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m14s
CI / backend-test-postgres (pull_request) Successful in 4m9s
56c8467cd7
--semantics-input-fields-border is a full `border` shorthand
("2px solid <color>"), not a color. Using it as
`border: 1px solid var(--semantics-input-fields-border, ...)` expands to
`1px solid 2px solid <color>`, invalid at computed-value time, so the whole
border drops (border-style resets to none). The "Aan" combobox input and the
selected-target chip therefore rendered with no visible frame. Use
--semantics-dividers-color (a real color), matching the Bericht box and the
reply composer.
The rule set only the surface-colored box-shadow spacer and omitted the colored
outline (--semantics-focus-ring-outline), so native focusable elements (the
format-toolbar buttons, the send button) got an invisible, same-as-surface halo
and no visible focus indicator. Add the outline + offset so keyboard focus is
visible app-wide (WCAG 2.4.7); NLDD web components keep their own shadow-DOM
ring.
Turn the shared Modal into the one shell every dialog uses: a `title` prop that
renders the standard nldd-title header + close button, a `#header` slot for
custom headers, an optional `#actions` footer slot, a `size` preset (sm/md/lg)
replacing inline --modal-max-width, and an owned scrollable `.body` region with
scroll-padding so focused children are not clipped. ComposeOverlay and
ShortcutHelp drop their hand-rolled headers/widths and adopt it; the compose
modal now uses `lg` (58rem), matching the card modal for a uniform width.
Add Field.vue + shared .field-box; de-duplicate the composer boxes
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
CI / pre-commit (pull_request) Successful in 43s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 22s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 30s
security-scan / Python SAST (bandit) (pull_request) Successful in 34s
CI / backend-test (pull_request) Successful in 1m15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
CI / frontend-test (pull_request) Successful in 1m38s
test-build / build (backend) (pull_request) Successful in 1m21s
test-build / build (frontend) (pull_request) Successful in 1m26s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m48s
CI / backend-test-postgres (pull_request) Successful in 4m31s
63888b2b8a
The bordered-box + focus-within look was defined identically in ComposeOverlay
(.editorWrap) and ReplyComposer (.wrapper). Extract it to one global
`.field-box` (styles/base.css) and a thin Field.vue (label + box + control
slot; metrics via --field-padding/--field-min-height). ComposeOverlay's Bericht
field now uses <Field>; ReplyComposer composes `.field-box` and keeps only its
layout, drop-target and send-state. One source of truth for the field look.
Replace nldd-modal-dialog (native <dialog>, imperative show()/updateComplete,
top-layer copy fallback) with the shared Modal: title + titleSize=5 + size=sm +
#actions footer for "Download bestand". ExportPanel already mounts it via v-if,
so the mount-based focus-trap/scroll-lock replaces the imperative dance. Update
the unit test (backdrop-click close) and the e2e selector (role="dialog" instead
of nldd-modal-dialog).
Replace its hand-rolled overlay/panel/focus-trap/scroll-lock/Escape/backdrop
with the shared Modal (size=lg). The quiet breadcrumb + source-link move to the
#header slot; the reading body stays a consumer div with --modal-body-padding: 0
so it keeps its 24px side inset and mobile overrides. ImageLightbox (teleports to
body) and the top-layer MentionProfilePopover keep their placement. Modal gains
an overridable body padding and a flex header row for the breadcrumb layout.
Despite the -Modal suffix it is an inline strip inside the card modal, not a
dialog (no overlay, no focus-trap - its own comment said so). Rename the file,
the props interface and ReplyComposer's import/usage + its test mock. No
behaviour change.
Update CHANGELOG for the modal/field consolidation + focus ring
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 23s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
CI / pre-commit (pull_request) Successful in 36s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 46s
CI / backend-test (pull_request) Successful in 1m33s
test-build / build (backend) (pull_request) Successful in 1m35s
CI / frontend-test (pull_request) Successful in 1m45s
test-build / build (frontend) (pull_request) Successful in 1m47s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m47s
CI / backend-test-postgres (pull_request) Successful in 4m29s
1ce2dd1a8b
Address adversarial review of the modal refactor (minor fixes)
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
CI / pre-commit (pull_request) Successful in 32s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 1m0s
test-build / build (backend) (pull_request) Successful in 1m1s
test-build / build (frontend) (pull_request) Successful in 1m8s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m17s
CI / e2e (pull_request) Successful in 3m27s
CI / backend-test-postgres (pull_request) Successful in 4m6s
b7f6f6305d
- mm-mention focus ring: drop the outline:none override so interactive mention
  pills get the global colored focus ring (WCAG 2.4.7), matching the base.css
  change instead of staying a self-inconsistent half-fix.
- Card modal header alignment: --modal-header-inset lets CardDetailsModal match
  the shared Modal's header inset to its 24px/8px reading body, so the breadcrumb
  lines up with the message column again (verified: both at 24px).
- Field: remove the unused labelId/useId (dead API); add click-to-focus on the
  box padding (mirrors ReplyComposer's handleWrapperClick) so the cursor: text
  affordance actually focuses the editor.
Merge remote-tracking branch 'origin/main' into fase-5-composer
All checks were successful
CI / pre-commit (pull_request) Successful in 1m27s
CI / frontend-test (pull_request) Successful in 5m53s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
security-scan / Python SAST (bandit) (pull_request) Successful in 39s
security-scan / JS SCA (npm audit) (pull_request) Successful in 39s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 22s
CI / backend-test (pull_request) Successful in 8m50s
security-scan / SBOM (trivy) (pull_request) Successful in 15s
test-build / build (backend) (pull_request) Successful in 1m57s
CI / backend-test-postgres (pull_request) Successful in 10m53s
test-build / build (frontend) (pull_request) Successful in 2m20s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 10m5s
4e27b8a90e
# Conflicts:
#	CHANGELOG.md
#	backend/waggle/api/outgoing_posts.py
#	frontend/src/api/outgoingPosts.ts
#	frontend/src/components/cards/CardDetailsModal.vue
#	frontend/src/components/composer/ReplyComposer.vue
#	frontend/src/components/scheduling/QueuedRow.vue
#	frontend/src/components/settings/PreferencesPanel.vue
#	frontend/src/components/settings/RecoveryKeyModal.vue
#	frontend/src/components/topnav/TopNav.vue
#	frontend/src/styles/base.css
Match Mattermost's 10-file cap and list card-less drafts under Concepten
All checks were successful
CI / pre-commit (pull_request) Successful in 55s
CI / frontend-test (pull_request) Successful in 5m32s
CI / release-scripts (pull_request) Successful in 9s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
security-scan / Python SAST (bandit) (pull_request) Successful in 35s
CI / backend-test (pull_request) Successful in 7m35s
security-scan / JS SCA (npm audit) (pull_request) Successful in 40s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
CI / backend-test-postgres (pull_request) Successful in 9m58s
test-build / build (backend) (pull_request) Successful in 1m58s
CI / e2e (pull_request) Successful in 9m37s
test-build / build (frontend) (pull_request) Successful in 2m20s
test-build / build (pull_request) Successful in 0s
57a1c92ac2
- Raise MAX_FILE_REFS / MAX_ATTACHMENTS from 5 to 10 to match Mattermost's real
  per-post file limit (the server rejects >300 runes of FileIds, i.e. 10 ids).
- Serialize the card-less new-message draft in GET /api/drafts (previously a 500
  that blanked the whole Concepten page) and show it under Concepten, reopening
  the composer on click.
Align TargetPicker to NLDD: icon-button, focus ring and shadow tokens
All checks were successful
CI / pre-commit (pull_request) Successful in 54s
CI / frontend-test (pull_request) Successful in 5m40s
CI / release-scripts (pull_request) Successful in 13s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 59s
security-scan / Python SAST (bandit) (pull_request) Successful in 35s
CI / backend-test (pull_request) Successful in 7m50s
security-scan / JS SCA (npm audit) (pull_request) Successful in 38s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 22s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test-postgres (pull_request) Successful in 9m58s
test-build / build (backend) (pull_request) Successful in 1m54s
CI / e2e (pull_request) Successful in 9m48s
test-build / build (pull_request) Successful in 0s
test-build / build (frontend) (pull_request) Successful in 2m16s
52d58cad34
Replace per-element --color-focus outlines with the shared NLDD focus ring
All checks were successful
CI / pre-commit (pull_request) Successful in 56s
CI / frontend-test (pull_request) Successful in 5m34s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 58s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
CI / backend-test (pull_request) Successful in 8m5s
security-scan / JS SCA (npm audit) (pull_request) Successful in 41s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 25s
CI / backend-test-postgres (pull_request) Successful in 10m14s
test-build / build (backend) (pull_request) Successful in 1m58s
test-build / build (frontend) (pull_request) Successful in 2m20s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 9m53s
62568553ab
Save the first new-message draft and keep Concepten in sync
All checks were successful
CI / pre-commit (pull_request) Successful in 53s
CI / frontend-test (pull_request) Successful in 5m58s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m1s
security-scan / Python SAST (bandit) (pull_request) Successful in 40s
CI / backend-test (pull_request) Successful in 8m28s
security-scan / JS SCA (npm audit) (pull_request) Successful in 42s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test-postgres (pull_request) Successful in 10m31s
test-build / build (backend) (pull_request) Successful in 1m55s
CI / e2e (pull_request) Successful in 10m12s
test-build / build (frontend) (pull_request) Successful in 2m21s
test-build / build (pull_request) Successful in 0s
82cb657ef7
The autosave stayed disabled for a first-time user: the seed guard never flipped
when the draft query resolved to null, so the very first message was never
persisted and never appeared under Concepten. Enable autosave once the query
resolves, invalidate the drafts list on save/delete, and let scheduling consume
the draft like send does.
Use normal case for labels and section headers (drop uppercase)
All checks were successful
CI / e2e (pull_request) Successful in 9m57s
CI / pre-commit (pull_request) Successful in 57s
CI / frontend-test (pull_request) Successful in 5m43s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
CI / backend-test (pull_request) Successful in 8m1s
security-scan / JS SCA (npm audit) (pull_request) Successful in 40s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 23s
security-scan / SBOM (trivy) (pull_request) Successful in 15s
CI / backend-test-postgres (pull_request) Successful in 10m16s
test-build / build (backend) (pull_request) Successful in 2m5s
test-build / build (frontend) (pull_request) Successful in 2m19s
test-build / build (pull_request) Successful in 0s
42fa398ff9
Card-less new-message drafts were a per-user singleton: a partial-unique
index (ux_outgoing_one_new_message_draft) plus an upsert meant starting a
new message overwrote the previous concept, so you could never keep two in
flight.

Make them id-based so a user can work on several messages at once:

- Drop the singleton index (migration 595995509a58); the per-card
  one-draft index stays.
- Service: replace the upsert with create/update/get/delete-by-id.
- API: POST creates (201); GET/PUT/DELETE target /{draft_id} and 404 on an
  unknown id. The schedule endpoint no longer deletes "the" draft; the
  composer deletes its own source draft by id after scheduling.
- Schemas expose the draft id.
- Frontend: the composer tracks the current draft id, autosave creates on
  first content then updates by id, "Nieuw bericht" opens blank while
  earlier concepts persist under Concepten, and reopening a concept
  re-seeds its target and content.
Two follow-ups from live testing of the new-message composer.

Drag-and-drop attachments
- Drag-and-drop did nothing in the new-message overlay; ComposeOverlay never
  wired the drop events. Extract the drop wiring into a shared useFileDropzone
  composable + an AttachmentDropOverlay component, used by both ComposeOverlay
  and ReplyComposer - one implementation, not two copies. Drag state resets on
  open because ComposeOverlay stays mounted between opens (App.vue).

Button cursors follow NLDD's convention
- Waggle forced a pointer cursor onto buttons via three mechanisms (a
  shadow-injected stylesheet, host rules on nldd-button/nldd-icon-button, and a
  token override), fighting NLDD's convention that the pointer is for links, not
  buttons. Drop the forcing: buttons - including nldd-icon-button (the modal
  close) and the send button - now use the default cursor. The pointer stays on
  links, clickable card/row surfaces, menu/combobox options, collapse toggles,
  checkboxes and the upload dropzone. Swept the per-component button rules to
  match.
Constrain the Server and Aan compose fields to a max-width
All checks were successful
CI / pre-commit (pull_request) Successful in 1m21s
CI / frontend-test (pull_request) Successful in 5m43s
CI / release-scripts (pull_request) Successful in 9s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 58s
security-scan / Python SAST (bandit) (pull_request) Successful in 36s
security-scan / JS SCA (npm audit) (pull_request) Successful in 39s
CI / backend-test (pull_request) Successful in 8m28s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 25s
security-scan / SBOM (trivy) (pull_request) Successful in 15s
CI / backend-test-postgres (pull_request) Successful in 10m37s
test-build / build (backend) (pull_request) Successful in 1m59s
CI / e2e (pull_request) Successful in 9m50s
test-build / build (frontend) (pull_request) Successful in 2m26s
test-build / build (pull_request) Successful in 0s
8141e33888
Server and "Aan" hold short values (a server / channel name), so stretching
them across the wide (size="lg") new-message modal looked oversized. Cap both
at 32rem, left-aligned; the message field stays full width. Layout-only, styled
around the NLDD components (they stay block-level by default).
- Ambiguous transport errors on create_post (read/write timeout, connection
  reset after send) now settle as failed with a Dutch explanation instead of
  retrying: the retry backoff (>=1 min) outlives Mattermost's 30s
  pending_post_id dedupe cache, so a retry after an accepted-but-timed-out
  post would duplicate it. Connect errors (request never sent) still retry.
- The due-row claim is now an atomic conditional UPDATE (state='scheduled'
  guard): a cancel that lands first is respected and a second process
  sharing the database cannot double-claim a row.
- The immediate reply endpoint accepts an optional idempotency_key and
  forwards it as pending_post_id, so a user retry after a timeout dedupes
  at Mattermost instead of double-posting.
- Send-triggered completion of a reading-list card now queues the MM unsave
  (with undo snapshot), matching the manual unread->read flow.
- Merge-mode dedupe for card-bound outgoing posts keyed on
  (card_id, idempotency_key=None), which never matches live rows (the UI
  always sets a key): re-importing a backup duplicated a still-pending
  scheduled post, which then fired twice. Dedupe now uses content-shaped
  keys (card_id, scheduled_at, content) like the card-less path.
- Import no longer enforces the dropped one-card-less-draft singleton;
  card-less drafts dedupe on target identity + content.
- Export/import now round-trips DM-targeted and target-less card-less
  drafts (new optional dm fields on OutgoingPostRecord, format stays v1).
- A card draft no longer shadows the same card's scheduled posts on import;
  preview and insert stay in step.
- Aware non-UTC scheduled_at values are normalized to UTC at the service
  boundary; SQLite silently dropped the offset before.
- WAGGLE_DEV_MM_MOCK now fails production startup fast, like the other
  dev flags.
- The 595995509a58 downgrade first keeps only the newest card-less draft
  per user before recreating the singleton unique index, so a downgrade
  on multi-draft data no longer aborts; tested on both dialects.
- Authorization tests for mention-candidates (foreign account 404,
  unauthenticated 401); stale Revises header fixed in 30a858a07271.
The composer deleted the concept at the moment Verzend was clicked, before
the 10s undo window. Deleting the draft reaped its parked attachment bytes
(no server row holds them during the client-side delay), so every card-less
send with attachments failed with 'file ref not found' 10 seconds later,
the promised concept restore failed on the same dead refs, and text plus
attachments were irrecoverably lost. Undo hit the same path.

- The draft now survives until POST /api/messages succeeds; fire() deletes
  it afterwards. Undo simply unlocks it; a failed send keeps it and shows
  an error toast. A crash or tab-close in the window no longer loses work.
- New lib/pendingDrafts.ts: a reactive lock set keeps a concept with an
  in-flight send out of the Concepten list and un-openable, plus per-draft
  attempt keys: one idempotency/pending_post_id per logical message, reused
  across retries, rotated on content/target changes - so Mattermost-side
  dedupe actually engages on a retry.
- In-flight guards: send/schedule buttons disabled while a schedule POST or
  pending send is active; autosave cannot create a new draft mid-send, an
  in-flight create is adopted into the attempt instead of orphaning a copy.
- Autosave timers are session-scoped: they die on close/reopen/draft-switch
  and can never write to the wrong concept; the schedule success handler
  deletes the id captured at request time.
- ReplyComposer: the server-draft-sync debounce is consumed before the
  draft delete, so a late PUT cannot resurrect a sent reply as a draft;
  replies send the new idempotency_key for MM-side dedupe.
- A failed or cancelled card-less scheduled message can be restored as a
  concept ("Zet terug naar concept"): content, server, target and
  attachments come back editable; ScheduledPostRead now serializes
  account_id so the server choice survives. The concept is created while
  the terminal row still holds its bytes, so the refs stay protected.
- Autosave failures in the new-message composer now show the SavedIndicator
  (like replies) instead of failing silently.
- Overview pages show a critical banner when the query fails instead of the
  empty-state text; drafts list rows key on the draft id.
- The Server select has an accessible name; failed rows keep full-contrast
  error text (tinted background instead of whole-row opacity).
- The unlink-account dialog names the real consequences (scheduled messages,
  concepts, attachments for that server are deleted).
- Query-cache hygiene: schedule creators invalidate the whole outgoing
  prefix; draft save/delete also invalidate the drafts list.
Update CHANGELOG for the review-round fixes
Some checks failed
CI / pre-commit (pull_request) Successful in 1m25s
CI / e2e (pull_request) Failing after 4m28s
CI / release-scripts (pull_request) Successful in 12s
CI / frontend-test (pull_request) Successful in 6m41s
security-scan / Python SCA (pip-audit) (pull_request) Failing after 1m5s
security-scan / Python SAST (bandit) (pull_request) Successful in 40s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 28s
security-scan / JS SCA (npm audit) (pull_request) Successful in 49s
security-scan / SBOM (trivy) (pull_request) Successful in 19s
CI / backend-test (pull_request) Successful in 9m31s
test-build / build (backend) (pull_request) Failing after 2m11s
test-build / build (frontend) (pull_request) Successful in 2m15s
test-build / build (pull_request) Failing after 0s
CI / backend-test-postgres (pull_request) Successful in 11m46s
77a8839c55
Harden composer-link e2e: wait for editor to clear before typing
Some checks failed
CI / pre-commit (pull_request) Successful in 57s
CI / frontend-test (pull_request) Successful in 6m6s
CI / release-scripts (pull_request) Successful in 11s
security-scan / Python SCA (pip-audit) (pull_request) Failing after 1m3s
security-scan / Python SAST (bandit) (pull_request) Successful in 42s
security-scan / JS SCA (npm audit) (pull_request) Successful in 46s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 25s
CI / backend-test (pull_request) Successful in 9m24s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test-postgres (pull_request) Successful in 11m20s
test-build / build (backend) (pull_request) Failing after 2m1s
CI / e2e (pull_request) Successful in 11m3s
test-build / build (frontend) (pull_request) Successful in 2m28s
test-build / build (pull_request) Failing after 0s
6609f2ee2c
The heading-clear step (Ctrl+A + Backspace) could race the next keystrokes,
leaving the heading text prepended to the link ('## Een ko[Klik hier]...').
Assert the editor is empty before typing so Playwright waits out the clear.
The shim stamped a pre-Alembic database (users table, no alembic_version)
straight to head, which since the initial revision skips 7 schema-changing
revisions and leaves the DB silently short of those columns. No released or
production database can reach this path: Alembic and the shim both shipped in
the repo's first commit, before the first release tag, so every deployed DB
got alembic_version on its first boot. The only thing it served was a
pre-repo prototype's stale dev SQLite file. Removing it makes such a DB fail
loudly on upgrade head (a reseed) instead of stamping it wrong.
- TargetPicker: the 'Aan' label used a for pointing at the combobox input,
  which unmounts once a chip is chosen, so the for dangled. Make the wrapper a
  role=group labelled by the 'Aan' text; the input keeps its aria-labelledby.
- MarkdownEditor: a bare contenteditable does not reliably carry an accessible
  name, so add role=textbox + aria-multiline alongside the aria-label (only
  when editable).
- QueuedRow: the icon-only pending actions (edit, send-now, cancel) had only a
  title, which is not a reliable accessible name; add aria-label and mark the
  icons aria-hidden.
The review-round bullets described fixing bugs in features that are new in
this same unreleased cycle (the composer, card-less drafts, new-message
scheduling, import of the new draft types). A user upgrading from the last
release never saw those bugs; the features simply ship correct. The
engineering detail stays in git log and the PR. Keep only genuinely
user-visible changes against the last release.
We compose a new message; "card-less" was never a user-facing concept. Rename
the term across comments, docstrings and the schema/API descriptions, the three
private import_service helpers (_new_message_key etc.), and two test files
(*_new_message.py) with their local helper. The data-layer condition itself is
still spelled out as "card_id IS NULL" where it matters. Migration files keep
the old wording until the migration squash rewrites them.
- main.py: trim the MM-mock veto comment to one line matching its siblings;
  the second sentence just restated the RuntimeError message.
- models/outgoing_post.py: reword "drafts are NOT unique per user" (index
  jargon) to say plainly there is deliberately no one-draft-per-user index.
- Rename the remaining card_less identifiers in tests to new_message.
fe3ff50e3e67 created the ux_outgoing_one_new_message_draft singleton index and
595995509a58 immediately dropped it again (to allow multiple drafts) - a
create-then-drop across two unreleased, this-PR-authored migrations, plus a
lossy multi-draft downgrade to make the recreate safe. Fold it: fe3ff50e3e67
never creates the index, 595995509a58 is gone. Net schema is identical; one
fewer migration and no downgrade-data-loss branch. Rename the file to match.

Also stub _run_migrations in the ensure_schema bootstrap tests: with the
pre-Alembic shim gone, the real upgrade over the create_all'd test schema
collides (surfaced on Postgres); these tests only cover the synthetic-user
bootstrap.

NOTE: fe3ff50e3e67 is already on main. A dev/CI database that applied the old
chain must be reseeded; production is unaffected (unreleased).
Tie the visible-attachment cap to the upload cap
Some checks failed
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 18s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
CI / pre-commit (pull_request) Successful in 29s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
test-build / build (backend) (pull_request) Successful in 58s
CI / backend-test (pull_request) Successful in 54s
test-build / build (frontend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m11s
CI / e2e (pull_request) Failing after 3m51s
CI / backend-test-postgres (pull_request) Successful in 4m29s
5e61cbd46a
MAX_VISIBLE_ATTACHMENTS was a separate 8; set it to MAX_ATTACHMENTS so a
message can never carry more thumbnails than its upload limit. The '+N'
overflow chip then only appears if the upload cap is later raised - a
deliberate cut-off rather than an independent magic number. Make the overflow
test symbolic so it follows the cap.
Changelog: introduce the new-message feature instead of enumerating it
Some checks failed
CI / release-scripts (pull_request) Successful in 6s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / JS SCA (npm audit) (pull_request) Successful in 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 39s
CI / backend-test (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 56s
test-build / build (frontend) (pull_request) Successful in 1m2s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m9s
CI / e2e (pull_request) Failing after 3m54s
CI / backend-test-postgres (pull_request) Successful in 4m41s
7685a925f0
The 'Nieuw bericht opstellen' section listed eleven bullets, many of them
sub-details of a single new feature (window width, drag-and-drop, smarter
search, task lists). Cut it to two: what the feature is, and that drafts
persist and can be multiple. Move the keyboard-shortcut opt-out to the
accessibility section. Drop the internal 'los van een kaart' framing (both
here and in the released backend note), matching the card-less -> new-message
rename.
Move MAX_FILE_REFS to config.py as a documented module constant
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 39s
CI / backend-test (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 59s
test-build / build (frontend) (pull_request) Successful in 1m7s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m11s
CI / e2e (pull_request) Failing after 3m53s
CI / backend-test-postgres (pull_request) Successful in 4m39s
69cb20c834
It lived in schemas/outgoing_post.py; config.py already collects the app's
limits, so it is a clearer home. Keep it a module-level constant, NOT a
Settings field: it is Mattermost's hard per-post file limit (10), not an
operator knob - env-raising it would only make the app accept files MM then
rejects at send time. All call sites (schema Fields, the MM client guard, the
reply schema) now import it straight from config; no re-export.
The opt-out (device-local localStorage store + the Instellingen toggle) is
pulled out so it can be redone account-scoped in its own PR, rather than
shipping a localStorage version here and migrating it later. The c/?//
shortcuts stay but are now always on.

KNOWN GAP: this leaves the new c and ? single-key shortcuts (and the existing
/) without a WCAG 2.1.4 opt-out until the follow-up lands. Tracked for the
account-scoped follow-up PR.
The 'Aan'-field search-icon fix is a fix to the new composer, not a change a
user of the last release would see. The single-key-shortcut opt-out bullet
goes with the opt-out itself being pulled from this PR.
Fix e2e for the raised attachment cap and the editor-clear focus
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
CI / pre-commit (pull_request) Successful in 29s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 55s
test-build / build (backend) (pull_request) Successful in 1m0s
test-build / build (frontend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m11s
CI / e2e (pull_request) Successful in 3m19s
CI / backend-test-postgres (pull_request) Successful in 4m34s
af29477f8f
- attachments-multi: MAX_VISIBLE_ATTACHMENTS rose from 8 to 10, so the fixed
  fixture counts (12 images, 9 pdfs) no longer overflowed the same way (9 pdfs
  stopped overflowing at all). Scale the overflow fixtures with the cap
  (MAX+3 images -> a +4 chip, MAX+2 pdfs -> a +3 chip) so they stay correct.
- composer-link: clicking 'Kop 2' in the toolbar dropdown moved focus out of
  the editor, so the Ctrl+A + Backspace clear did nothing and the heading text
  leaked into the sent message. Re-focus the editor before clearing (this was
  the root cause of the earlier flake).
The composer shows a URL.createObjectURL() thumbnail of a just-picked image
before it uploads, but img-src was 'self' data:, so the browser blocked the
blob: URL and drew a broken-image icon. Add blob: (the page can only show
blobs it created itself) and assert it in the HTML-response security test.
Changelog: introduce the unreleased features instead of enumerating them
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
CI / backend-test (pull_request) Successful in 57s
test-build / build (backend) (pull_request) Successful in 59s
test-build / build (frontend) (pull_request) Successful in 1m7s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m14s
CI / e2e (pull_request) Successful in 3m21s
CI / backend-test-postgres (pull_request) Successful in 4m37s
814b79ce9c
Apply the same trim to the remaining unreleased sections: merge the two terms
sections into one, cut Overzichtspagina's to the new pages + the outbox
sections (pulling the pages intro out of Automatisch opruimen), Bijlagen and
Automatisch opruimen to their essence, and drop the outbox-preview-overflow fix
(a fix to a feature new this cycle).
robbertbos force-pushed fase-5-composer from 814b79ce9c
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
CI / backend-test (pull_request) Successful in 57s
test-build / build (backend) (pull_request) Successful in 59s
test-build / build (frontend) (pull_request) Successful in 1m7s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m14s
CI / e2e (pull_request) Successful in 3m21s
CI / backend-test-postgres (pull_request) Successful in 4m37s
to 0fd75a9814
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
security-scan / Python SAST (bandit) (pull_request) Successful in 32s
security-scan / SBOM (trivy) (pull_request) Successful in 12s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
CI / pre-commit (pull_request) Successful in 41s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 51s
CI / backend-test (pull_request) Successful in 1m10s
CI / frontend-test (pull_request) Successful in 1m24s
test-build / build (backend) (pull_request) Successful in 58s
test-build / build (frontend) (pull_request) Successful in 1m2s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m26s
CI / backend-test-postgres (pull_request) Successful in 4m38s
2026-08-05 13:32:25 +00:00
Compare
robbertbos deleted branch fase-5-composer 2026-08-05 13:37:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!192
No description provided.