Skip to content

A channel that points at the organisation, not at the resident - #149

Merged
github-actions[bot] merged 1 commit into
masterfrom
feat/complaints-channel
Sep 1, 2026
Merged

A channel that points at the organisation, not at the resident#149
github-actions[bot] merged 1 commit into
masterfrom
feat/complaints-channel

Conversation

@catomean

@catomean catomean commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

The report form has offered exactly two destinations: a dripping tap to the maintenance board, a roommate conflict to the incident ladder. An objection to how AOZ itself acted fitted neither.

And filing one as an Incident would have been worse than dropping it — that ladder escalates toward a resident and ends in FORMAL_MEASURE. Complaining about staff would have opened a case against the person complaining.

The City's Eigentümerstrategie 2025–2028 fixes „Information und Beschwerdestellen" as one of six minimum standards in AOZ's Leistungsauftrag. AOZ's own central Beschwerdestelle logged 88 → 145 → 242 complaints across 2023–2025, 38% of the last figure about Unterbringung und Zusammenleben — while the client count stayed flat. The product had no side of that obligation.

The boundary is the feature

complaints:read and complaints:respond are held by no care role, and — the part that matters — are not widened by ALL_DOMAINS.

Every other permission in this product grows with oversight, because seeing every domain is the point of that axis. These must not: the person with oversight over every care domain is one of the people a complaint can be about. A grievance channel whose reader may be its subject is not a grievance channel.

They're named separately from SYSTEM_ADMIN_PERMISSIONS even though isSystemAdmin grants both today, because configuring the product and hearing a complaint against staff are different jobs. AOZ's Beschwerdestelle is deliberately independent of the site team; when a real person fills that seat they should get these two verbs without also being handed the settings page — the whole lesson of retiring ADMIN.

Anonymity, with its cost stated

residentId is nullable and the null is the anonymity — there is nothing to redact later because nothing was written. The route calls no logAudit for the same reason: an audit row naming the reporter would quietly undo the promise the form makes.

The trade-off sits next to the checkbox rather than buried: anonymous means nobody can answer, and it won't appear under „Deine Meldungen". Burying that would let someone choose anonymity and then wait for a reply that can never arrive.

Two gates caught me

  • Three portal gates rejected the first version, which used the staff German constants. Rightly: a complaints channel that only speaks German is close to useless for the people it exists to protect, who are the least likely in the building to read it. Now translated into all six offered locales.
  • The i18n script gate then caught a Ukrainian apostrophe — I'd used U+02BC where that dictionary uses U+2019. Same sibling-language class as the ru file that was once one-third Ukrainian.

Mutation-proven

Removing the early return in hasPermission and granting complaints:read to BETREUUNG — the exact "tidy up the special case" edit someone will eventually attempt — fails three checks.


202 suites / 3563 passed. SESSION_SECRET=… npm run build exit 0, all three routes compile. Contains a migration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h

…he resident

The report form has offered exactly two destinations: a dripping tap to the
maintenance board, a roommate conflict to the incident ladder. An objection to
how AOZ itself acted fitted neither — and filing one as an Incident would have
been worse than dropping it, because that ladder escalates TOWARD a resident
and ends in FORMAL_MEASURE. Complaining about staff would have opened a case
against the person complaining.

The City's Eigentümerstrategie 2025-2028 fixes "Information und
Beschwerdestellen" as one of six minimum standards in AOZ's Leistungsauftrag.
AOZ's own central Beschwerdestelle logged 88 complaints in 2023, 145 in 2024
and 242 in 2025 — 38% of the last figure about Unterbringung und
Zusammenleben, while the client count stayed flat. The product had no side of
that obligation at all.

THE BOUNDARY IS THE FEATURE. `complaints:read` and `complaints:respond` are
held by no care role, and — the part that matters — are NOT widened by
ALL_DOMAINS. Every other permission grows with oversight, because seeing every
domain is the point of that axis. These must not: the person with oversight
over every care domain is one of the people a complaint can be ABOUT. A
grievance channel whose reader may be its subject is not a grievance channel.

They are named separately from SYSTEM_ADMIN_PERMISSIONS even though
isSystemAdmin grants both today, because configuring the product and hearing a
complaint against staff are different jobs. AOZ's Beschwerdestelle is
deliberately independent of the site team; when a real person fills that seat
they should get these two verbs without also being handed the settings page,
which is the whole lesson of retiring ADMIN.

Anonymity is offered, with its cost stated where the choice is made: no name
means nobody can answer, and it will not appear under "Deine Meldungen".
`residentId` is nullable and the null IS the anonymity — there is nothing to
redact later because nothing was written. The route calls no logAudit for the
same reason: an audit row naming the reporter would quietly undo the promise.

Resident-facing copy is translated into all six offered locales. The first
version used the staff German constants and three portal gates caught it,
rightly — a complaints channel that only speaks German is close to useless for
the people it exists to protect, who are the least likely in the building to
read it. The i18n script gate then caught a Ukrainian apostrophe (U+02BC where
that dictionary uses U+2019), which is the same sibling-language class as the
`ru` file that was one-third Ukrainian.

Gated by complaint-boundary.test.ts. Mutation-proven: removing the early
return in `hasPermission` and granting `complaints:read` to BETREUUNG — the
exact "tidy up the special case" edit someone will eventually attempt — fails
three checks.

Verified with SESSION_SECRET=… npm run build (exit 0); all three routes
compile. Contains a migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cd183M6472xBgTKWA2is6h
@github-actions
github-actions Bot merged commit 28e3a08 into master Sep 1, 2026
4 checks passed
@github-actions
github-actions Bot deleted the feat/complaints-channel branch September 1, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant