Choose whose notes reach your bell
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m49s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m24s

The bell is a shared pile: every active HR person sees every open note,
regardless of who wrote it. That was agreed and it stays the default —
this narrows it, it never widens it. You can now untick colleagues whose
notes you do not want to see.

What gets stored is the *exceptions*, not the selection. The difference
shows the day someone new joins HR: had the selection been stored, she
would be invisible to everyone until each person ticked her, and nobody
would notice her follow-ups piling up. This way she is visible from day
one and hiding her is a deliberate act. Same reasoning that made notes a
shared inbox in the first place — the silent gap is worse than a row too
many.

Own notes always come through: `note_mutes` rejects a self-reference, and
the predicate says so again rather than depending on a check constraint
staying put. Notes with no author come through too — hiding one because
nobody knows who wrote it is exactly the loss this list exists to prevent.

The rule lives in lib/notes.ts as one SQL expression because two places
need it: the bell in the header and the "Anstehend" card on the dashboard.
Two copies drift, and then the card counts something the bell does not
show.

No SQL function and no audit row, unlike anything that touches employee
data — this is a personal display preference, and an audit trail recording
every tick would make finding real changes harder. Same pattern as saved
reports and hire drafts, and the owner policy on note_mutes means a row
for someone else cannot be written even with invented values.

The checkbox flips immediately and flips back if saving fails; the list
gets clicked through several at a time and a round trip per tick feels
like hesitation.

Verified: 19 tests, five mutation-checked (or→and, dropping the own-notes
clause, inverting `not exists`, inverting the default, and losing the
email fallback each turn them red). Typecheck, lint, schema drift, 477
tests and the build are clean. Not seen in a browser: login goes through
the company account and the database is unreachable — the migration is
reviewed but has not been run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 20:00:13 +02:00
parent e1b69fb022
commit 99b1df9735
12 changed files with 614 additions and 15 deletions

View File

@@ -0,0 +1,83 @@
-- Welche Kolleg:innen man in seinen Benachrichtigungen sehen will.
--
-- ═══ Was sich damit ändert ═══
--
-- Bisher war die Glocke ein gemeinsamer Topf: jede aktive HR-Person sah jede
-- offene Notiz, unabhängig davon, wer sie verfasst hat. Das war so
-- abgestimmt und bleibt der Standard — die Einstellung engt ein, sie öffnet
-- nichts.
--
-- ═══ Warum hier die Ausnahmen stehen und nicht die Auswahl ═══
--
-- Eine Zeile heisst: „die Notizen dieser Person will ich nicht sehen."
-- Gespeichert wird also das Abgewählte, nicht das Gewählte.
--
-- Der Unterschied fällt auf, sobald jemand Neues in die Personalabteilung
-- kommt. Stünde hier die Auswahl, wäre die neue Kollegin für alle unsichtbar,
-- bis jede einzelne Person sie anhakt — und niemandem fiele auf, dass ihre
-- Wiedervorlagen liegenbleiben. So herum ist sie ab dem ersten Tag sichtbar,
-- und wer sie ausblenden will, tut das ausdrücklich.
--
-- Dieselbe Überlegung wie bei den Notizen selbst: die stille Lücke ist
-- schlimmer als eine Zeile zu viel.
create table if not exists note_mutes (
-- Wessen Einstellung das ist.
user_id uuid not null references app_users(id) on delete cascade,
-- Wessen Notizen ausgeblendet werden.
muted_user_id uuid not null references app_users(id) on delete cascade,
created_at timestamptz not null default now(),
primary key (user_id, muted_user_id),
-- Die eigenen Notizen lassen sich nicht abwählen. Sie sind der Grund,
-- warum es die Glocke gibt; ein Haken, der die eigene Wiedervorlage
-- verschwinden lässt, wäre eine Falle.
constraint chk_note_mutes_nicht_selbst check (user_id <> muted_user_id)
);
comment on table note_mutes is
'Abgewählte Kolleg:innen je Person. Eine Zeile blendet die Notizen von muted_user_id für user_id aus. Ohne Zeile ist alles sichtbar.';
-- Der Zugriffsweg fragt immer „meine Ausnahmen": ohne Index ein Tabellen-
-- scan je Seitenaufruf, mit ihm ein Indexzugriff. Bei einer Handvoll HR-
-- Personen ist das heute belanglos und morgen nicht mehr.
create index if not exists idx_note_mutes_user on note_mutes (user_id);
alter table note_mutes enable row level security;
-- Eigentümergebunden wie hire_drafts und saved_reports: man sieht und
-- ändert ausschliesslich die eigenen Zeilen. Eine fremde Einstellung geht
-- niemanden etwas an — auch keine andere HR-Person.
drop policy if exists "note_mutes_owner" on note_mutes;
create policy "note_mutes_owner" on note_mutes
for all
using (user_id = app_current_user_id() and is_hr_user())
with check (user_id = app_current_user_id() and is_hr_user());
-- ═══ Gegenprobe ═══════════════════════════════════════════════════
do $$
begin
if not exists (
select 1 from pg_tables where tablename = 'note_mutes' and rowsecurity
) then
raise exception 'note_mutes hat keinen Zeilenschutz.';
end if;
if not exists (
select 1 from pg_policies where tablename = 'note_mutes' and policyname = 'note_mutes_owner'
) then
raise exception 'Die Eigentümerregel auf note_mutes fehlt.';
end if;
-- Der Selbstbezug muss abgewiesen werden. Ohne diese Prüfung liesse sich
-- die eigene Wiedervorlage ausblenden.
begin
insert into note_mutes (user_id, muted_user_id)
values ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001');
raise exception 'Eine Person kann sich selbst abwaehlen — die Pruefbedingung greift nicht.';
exception
when check_violation then null; -- so soll es sein
when foreign_key_violation then null; -- Fremdschluessel zuerst: ebenfalls abgewiesen
end;
end $$;