Retention for finished messages and completed cards, plus overview pages #191

Merged
robbertbos merged 1 commit from retention-and-overview-pages into main 2026-07-26 20:41:46 +00:00
Owner

Waggle kept everything forever. This adds automatic cleanup, under one rule:

Waggle never deletes what you added yourself or have not finished.

What is cleaned up, and what never is

Two per-user settings, each off / 30 / 90 / 180 / 365 days, defaulting to 90 for new accounts.

Cleaned up Kept, always
Sent, cancelled and failed outgoing messages, with their attachments Drafts
Completed and read cards Cards carrying a note

A completed card is also spared while it still has a live outgoing message attached, or while it still owes Mattermost an unsave (cleanup_status pending/running/failed). That last guard matters: deleting such a card would leave the message flagged "saved" in Mattermost with nothing left in Waggle to un-flag it.

What disappears is Waggle's own copy. The Mattermost messages stay where they are. Only a message that was never sent is genuinely lost, and the settings copy says so.

Accounts predating the migration start switched off. Otherwise the first nightly sweep would have deleted years of archive before anyone had seen the setting. Those users get a one-time banner offering to switch it on at 90 days.

Three pages for what is kept

Reachable from the menu, rendered with the app's own card components rather than a bespoke list:

  • Concepten (/concept-berichten) - a draft lived in no list at all before this, because both scheduled-post endpoints filter drafts out. A half-written reply was unreachable without opening the exact card it hung on.
  • Uitgaande post (/uitgaande-post) - everything outgoing in three sections, failures first. Outgoing messages previously lived as sections inside the card lists, and sent messages were visible nowhere.
  • Notities (/notities) - the cards that are never cleaned up.

Two defects that were not in the issue

Both found during review, both closed, each pinned by a test that fails against the bug:

  1. Cancelled and failed rows shielded their attachments from the 7-day orphan sweep indefinitely. That sweep built its referenced set from select(OutgoingPost.files) with no state filter, so a dead row kept its attachment alive. Nothing cleaned up on cancel or on a failed send; only the manual "Gelezen, verberg" button did.
  2. Deleting a completed card took its drafts with it through the FK cascade, including their attachment bytes, because the ref query had no state filter either. Reachable in practice: you can write a reply on a card you completed months ago.

Attachment bytes are released only after the deleting transaction commits, and only through _delete_orphan_refs, because a ref can legitimately be held by a live draft - editing a scheduled post cancels the row and re-hangs its files on the new draft.

Also here

The terms are re-readable on /voorwaarden, with the date they were accepted, from a single shared component so the retention sentence cannot drift between the two places. /welkom drops its connect-Mattermost step: a server is linked from Instellingen, and a new user without one gets a banner pointing there.

Accepted trade-offs

  • A user can switch cleanup off entirely, so an operator cannot treat storage limitation as a system guarantee. Stated plainly in docs/deployment.md rather than left implicit.
  • Existing accounts get no storage limitation until they act. The alternative was an archive deleted on deploy night.
  • No warning before a card is removed. The user ticked the card off themselves, set the period themselves, and everything they added is spared regardless.

Issue #146

Closed with a different solution than it asked for. The issue wanted a maximum age on draft attachments; drafts are kept instead, and answered with a findable list. docs/deployment.md is updated accordingly.

Tested

Backend 1610 tests at 100% branch coverage, on SQLite and Postgres. Frontend 570 unit tests, 63 e2e. The migration's data step - the line that decides whether a deploy destroys data - has its own tests, because alembic/versions/ sits outside the coverage gate.

Separately filed: #205, our universal CSS reset strips the padding from NLDD components that style their host, and MinBZK/storybook#160 as the upstream hardening suggestion.

Waggle kept everything forever. This adds automatic cleanup, under one rule: > **Waggle never deletes what you added yourself or have not finished.** ## What is cleaned up, and what never is Two per-user settings, each off / 30 / 90 / 180 / 365 days, defaulting to 90 for new accounts. | Cleaned up | Kept, always | | --- | --- | | Sent, cancelled and failed outgoing messages, with their attachments | Drafts | | Completed and read cards | Cards carrying a note | A completed card is also spared while it still has a live outgoing message attached, or while it still owes Mattermost an unsave (`cleanup_status` pending/running/failed). That last guard matters: deleting such a card would leave the message flagged "saved" in Mattermost with nothing left in Waggle to un-flag it. What disappears is Waggle's own copy. The Mattermost messages stay where they are. Only a message that was never sent is genuinely lost, and the settings copy says so. **Accounts predating the migration start switched off.** Otherwise the first nightly sweep would have deleted years of archive before anyone had seen the setting. Those users get a one-time banner offering to switch it on at 90 days. ## Three pages for what is kept Reachable from the menu, rendered with the app's own card components rather than a bespoke list: - **Concepten** (`/concept-berichten`) - a draft lived in no list at all before this, because both scheduled-post endpoints filter drafts out. A half-written reply was unreachable without opening the exact card it hung on. - **Uitgaande post** (`/uitgaande-post`) - everything outgoing in three sections, failures first. Outgoing messages previously lived as sections inside the card lists, and sent messages were visible nowhere. - **Notities** (`/notities`) - the cards that are never cleaned up. ## Two defects that were not in the issue Both found during review, both closed, each pinned by a test that fails against the bug: 1. **Cancelled and failed rows shielded their attachments from the 7-day orphan sweep indefinitely.** That sweep built its `referenced` set from `select(OutgoingPost.files)` with no state filter, so a dead row kept its attachment alive. Nothing cleaned up on cancel or on a failed send; only the manual "Gelezen, verberg" button did. 2. **Deleting a completed card took its drafts with it** through the FK cascade, including their attachment bytes, because the ref query had no state filter either. Reachable in practice: you can write a reply on a card you completed months ago. Attachment bytes are released only after the deleting transaction commits, and only through `_delete_orphan_refs`, because a ref can legitimately be held by a live draft - editing a scheduled post cancels the row and re-hangs its files on the new draft. ## Also here The terms are re-readable on `/voorwaarden`, with the date they were accepted, from a single shared component so the retention sentence cannot drift between the two places. `/welkom` drops its connect-Mattermost step: a server is linked from Instellingen, and a new user without one gets a banner pointing there. ## Accepted trade-offs - A user can switch cleanup off entirely, so an operator cannot treat storage limitation as a system guarantee. Stated plainly in `docs/deployment.md` rather than left implicit. - Existing accounts get no storage limitation until they act. The alternative was an archive deleted on deploy night. - No warning before a card is removed. The user ticked the card off themselves, set the period themselves, and everything they added is spared regardless. ## Issue #146 Closed with a different solution than it asked for. The issue wanted a maximum age on draft attachments; drafts are kept instead, and answered with a findable list. `docs/deployment.md` is updated accordingly. ## Tested Backend 1610 tests at 100% branch coverage, on SQLite and Postgres. Frontend 570 unit tests, 63 e2e. The migration's data step - the line that decides whether a deploy destroys data - has its own tests, because `alembic/versions/` sits outside the coverage gate. Separately filed: [#205](https://code.overheid.nl/robbertbos/waggle/issues/205), our universal CSS reset strips the padding from NLDD components that style their host, and [MinBZK/storybook#160](https://github.com/MinBZK/storybook/issues/160) as the upstream hardening suggestion.
robbertbos force-pushed retention-and-overview-pages from 7892758a64
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / pre-commit (pull_request) Successful in 55s
test-build / build (frontend) (pull_request) Successful in 53s
test-build / build (backend) (pull_request) Successful in 58s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m41s
CI / frontend-test (pull_request) Successful in 4m6s
CI / backend-test (pull_request) Successful in 5m10s
CI / backend-test-postgres (pull_request) Successful in 8m52s
to 2aaad960a8
All checks were successful
CI / pre-commit (pull_request) Successful in 1m25s
CI / frontend-test (pull_request) Successful in 5m4s
CI / release-scripts (pull_request) Successful in 11s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m2s
security-scan / Python SAST (bandit) (pull_request) Successful in 41s
security-scan / JS SCA (npm audit) (pull_request) Successful in 48s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test (pull_request) Successful in 8m29s
test-build / build (backend) (pull_request) Successful in 2m2s
CI / backend-test-postgres (pull_request) Successful in 10m48s
test-build / build (frontend) (pull_request) Successful in 2m25s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 9m41s
2026-07-25 21:50:01 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from 2aaad960a8
All checks were successful
CI / pre-commit (pull_request) Successful in 1m25s
CI / frontend-test (pull_request) Successful in 5m4s
CI / release-scripts (pull_request) Successful in 11s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m2s
security-scan / Python SAST (bandit) (pull_request) Successful in 41s
security-scan / JS SCA (npm audit) (pull_request) Successful in 48s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / backend-test (pull_request) Successful in 8m29s
test-build / build (backend) (pull_request) Successful in 2m2s
CI / backend-test-postgres (pull_request) Successful in 10m48s
test-build / build (frontend) (pull_request) Successful in 2m25s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 9m41s
to f950032cfb
All checks were successful
CI / release-scripts (pull_request) Successful in 11s
security-scan / SBOM (trivy) (pull_request) Successful in 15s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / Python SAST (bandit) (pull_request) Successful in 31s
security-scan / JS SCA (npm audit) (pull_request) Successful in 33s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 52s
CI / backend-test (pull_request) Successful in 1m17s
CI / pre-commit (pull_request) Successful in 1m28s
CI / frontend-test (pull_request) Successful in 1m29s
test-build / build (frontend) (pull_request) Successful in 1m23s
test-build / build (backend) (pull_request) Successful in 2m2s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 4m9s
CI / backend-test-postgres (pull_request) Successful in 4m50s
2026-07-26 06:58:46 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from 1839e06718
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 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 18s
security-scan / Python SAST (bandit) (pull_request) Successful in 19s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
test-build / build (backend) (pull_request) Successful in 1m1s
test-build / build (frontend) (pull_request) Successful in 1m1s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 1m6s
CI / frontend-test (pull_request) Successful in 1m14s
CI / pre-commit (pull_request) Successful in 1m17s
CI / e2e (pull_request) Successful in 3m58s
CI / backend-test-postgres (pull_request) Successful in 4m53s
to b84b34b176
All checks were successful
CI / pre-commit (pull_request) Successful in 56s
CI / frontend-test (pull_request) Successful in 5m10s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 59s
security-scan / Python SAST (bandit) (pull_request) Successful in 40s
CI / e2e (pull_request) Successful in 9m15s
CI / backend-test (pull_request) Successful in 7m41s
security-scan / JS SCA (npm audit) (pull_request) Successful in 43s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 23s
test-build / build (backend) (pull_request) Successful in 1m55s
CI / backend-test-postgres (pull_request) Successful in 10m5s
test-build / build (frontend) (pull_request) Successful in 2m17s
test-build / build (pull_request) Successful in 0s
2026-07-26 10:08:32 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from a97ee477fc
Some checks failed
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 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 20s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
CI / pre-commit (pull_request) Successful in 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / backend-test (pull_request) Successful in 58s
test-build / build (backend) (pull_request) Successful in 1m0s
test-build / build (frontend) (pull_request) Successful in 1m8s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m15s
CI / backend-test-postgres (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
to b326d83f75
All checks were successful
CI / release-scripts (pull_request) Successful in 11s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / JS SCA (npm audit) (pull_request) Successful in 26s
security-scan / Python SAST (bandit) (pull_request) Successful in 28s
CI / pre-commit (pull_request) Successful in 34s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 47s
CI / backend-test (pull_request) Successful in 1m5s
test-build / build (backend) (pull_request) Successful in 1m2s
test-build / build (frontend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m20s
CI / e2e (pull_request) Successful in 3m9s
CI / backend-test-postgres (pull_request) Successful in 4m15s
2026-07-26 19:18:09 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from b326d83f75
All checks were successful
CI / release-scripts (pull_request) Successful in 11s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / JS SCA (npm audit) (pull_request) Successful in 26s
security-scan / Python SAST (bandit) (pull_request) Successful in 28s
CI / pre-commit (pull_request) Successful in 34s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 47s
CI / backend-test (pull_request) Successful in 1m5s
test-build / build (backend) (pull_request) Successful in 1m2s
test-build / build (frontend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m20s
CI / e2e (pull_request) Successful in 3m9s
CI / backend-test-postgres (pull_request) Successful in 4m15s
to 9b94083420
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 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 56s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m24s
CI / e2e (pull_request) Successful in 3m37s
CI / frontend-test (pull_request) Successful in 4m46s
CI / backend-test (pull_request) Successful in 6m16s
CI / backend-test-postgres (pull_request) Has been cancelled
2026-07-26 20:11:10 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from 9b94083420
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 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 56s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m24s
CI / e2e (pull_request) Successful in 3m37s
CI / frontend-test (pull_request) Successful in 4m46s
CI / backend-test (pull_request) Successful in 6m16s
CI / backend-test-postgres (pull_request) Has been cancelled
to 20e73883b1
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
security-scan / Python SAST (bandit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 48s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
test-build / build (backend) (pull_request) Successful in 1m16s
CI / backend-test (pull_request) Successful in 1m31s
CI / frontend-test (pull_request) Successful in 1m46s
test-build / build (frontend) (pull_request) Successful in 1m48s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m24s
CI / backend-test-postgres (pull_request) Successful in 4m30s
2026-07-26 20:17:43 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from 20e73883b1
All checks were successful
CI / release-scripts (pull_request) Successful in 10s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 31s
security-scan / Python SAST (bandit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 48s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
test-build / build (backend) (pull_request) Successful in 1m16s
CI / backend-test (pull_request) Successful in 1m31s
CI / frontend-test (pull_request) Successful in 1m46s
test-build / build (frontend) (pull_request) Successful in 1m48s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m24s
CI / backend-test-postgres (pull_request) Successful in 4m30s
to 54bfffd2ca
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 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 53s
CI / pre-commit (pull_request) Successful in 56s
test-build / build (backend) (pull_request) Successful in 57s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m3s
CI / backend-test-postgres (pull_request) Has been cancelled
CI / frontend-test (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
2026-07-26 20:24:54 +00:00
Compare
robbertbos force-pushed retention-and-overview-pages from 54bfffd2ca
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 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 53s
CI / pre-commit (pull_request) Successful in 56s
test-build / build (backend) (pull_request) Successful in 57s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 3m3s
CI / backend-test-postgres (pull_request) Has been cancelled
CI / frontend-test (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
to c4f334409b
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 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
CI / pre-commit (pull_request) Successful in 31s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / backend-test (pull_request) Successful in 1m1s
test-build / build (backend) (pull_request) Successful in 1m2s
test-build / build (frontend) (pull_request) Successful in 1m8s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m18s
CI / e2e (pull_request) Successful in 3m6s
CI / backend-test-postgres (pull_request) Successful in 4m14s
CI / release-scripts (push) Successful in 5s
security-scan / SBOM (trivy) (push) Successful in 9s
security-scan / Filesystem scan (trivy fs) (push) Successful in 12s
security-scan / JS SCA (npm audit) (push) Successful in 17s
security-scan / Python SAST (bandit) (push) Successful in 18s
security-scan / Python SCA (pip-audit) (push) Successful in 38s
publish-main / build (frontend) (push) Successful in 57s
publish-main / build (backend) (push) Successful in 1m0s
publish-main / build (push) Successful in 0s
CI / pre-commit (push) Successful in 1m25s
CI / e2e (push) Successful in 2m57s
CI / frontend-test (push) Successful in 4m19s
CI / backend-test (push) Successful in 5m42s
CI / backend-test-postgres (push) Successful in 9m59s
2026-07-26 20:28:08 +00:00
Compare
robbertbos deleted branch retention-and-overview-pages 2026-07-26 20:41:46 +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!191
No description provided.