Zuständigkeit auf Fristen und Termine — assignee field, plus "meine Fristen" everywhere it should exist #161
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
m, 2026-07-30, answering the #160 calendar question:
The gap
paliad.deadlinesandpaliad.appointmentshave no assignee field. The only per-user column iscreated_by— whoever typed the row in, which is not the same person as whoever is responsible for it.Measured while designing #160 (cronus, 2026-07-29): there is no assignee, owner or responsible column on either table.
Why it is bigger than a column
The same missing concept blocks three separate things:
my_deadlinescalendar feed scope — deliberately kept out of #160 for this reason. #160 ships the per-user feed (which already works) and the hierarchy scopes; a feed scoped to my deadlines waits on this issue.Scope
deadlinesand onappointments, nullable — an unassigned deadline is the normal case and must not be forced to a person.Out of scope
Calendar feed scopes — that is #160.
Filed by paliad/head from m's ruling of 2026-07-30. The reminder scanner's audience predicate already reaches beyond
can_see_projectviaescalation_contact_id, so readdocs/findings-rls-s6-preflight-rehearsal-2026-07-29.md§9 before deciding how assignment interacts with visibility.The visibility half of this issue already has its core — reuse it, do not rebuild it
m's escalation ruling of the same day is implemented on
mai/brunel/escalation-visibility-flag(beb133b, design atdocs/design-escalation-visibility-flag-2026-07-30.md). It was built to serve this issue too, so the assignee work should add an enumerator and nothing else.What is already there —
internal/services/visibility_mismatch_service.go:CanSeeProjects(ctx, userID, projectIDs) → map[uuid]bool— the core. Answers "can this user see these matters" for any user, from any context.VisibilityMismatchcarries arelationlabel rather than being named after escalation.RelationEscalationContactexists;RelationAssigneeis the one this issue adds./admin/escalation-visibilitypage and its count badge render whatever relations the list contains, so an assignee row appears there with no new page.What this issue supplies: the pairs. Escalation reads them from
users.escalation_contact_id× the owner's pending deadlines' projects; assignment reads them fromdeadlines.assignee_id/appointments.assignee_id× that row's project.Three things that will bite if they are re-derived rather than reused — each is a measured property, not a preference:
Ask
paliad.can_see_projectAS the user, viadb.WithActingUser. Do not write the predicate in Go.internal/services/visibility.goalready carries a mirror of it and that mirror drifted from the function for two months over mig 055's partner-unit branch. A check whose job is to report a disagreement must not be a third implementation that can disagree with both.Do not implement the check as "run the query as them and see whether rows come back." That reports zero under today's permissive prod, because the bypassing role returns every row. It would go live at the S6 flip — dark for exactly the window the flag exists to cover.
TestVisibilityMismatch_SameUnderBothRolesrequires the identical answer under the bypassing role and underpaliad_app, and that implementation fails it while passing everything else.FROM unnest($1::uuid[]), neverFROM paliad.projects.projectsis itself gated bycan_see_project, so an invisible project yields no row and "this user cannot see it" collapses into "it does not exist".The one thing this issue must decide for itself: what an assignee who cannot see the matter means. For escalation the answer was settled by the harm — the reminder simply does not go, and
visibleForCategorysuppresses the global-admin fallback whenever a contact is named, so the overdue escalation channel ends up empty. Assignment's harm is different: a deadline assigned to somebody who cannot open it. Whether that should be flagged only, warned at assign time, or refused outright is m's call, and it is a different question from the escalation one even though the shape is the same.Verification pattern worth copying:
cmd/server/http_smoke_escalation_visibility_test.gomeasures the consequence over HTTP under an enforcing role — the named user's own read of the matter returns zero, and returns rows once they are staffed — rather than asserting that a flag appeared.Not closing; only m closes issues.
Gebaut, auf
mai/hades/issue-161-zustandigkeitMigration 213 —
assigned_toaufpaliad.deadlinesundpaliad.appointments. Nullable,ON DELETE SET NULL, Teil-Index für den "meine Fristen"-Read. Prod steht auf 212, die Nummer ist frei.Commits (branch, gepusht):
Die Entscheidung, um die der Issue explizit gebeten hat
Zuständigkeit gibt keine Sichtbarkeit.
code: "assignee_not_visible".assignee_can_seemeldet die Drift, die eine Ablehnung nicht verhindern kann.Beide Hälften sind nötig. Zugriff hängt am Team. Eine spätere Team-Änderung nimmt ihn weg, ohne dass eine Frist geschrieben wird — in dem Moment läuft keine Prüfung, weil nichts gespeichert wurde. Nur ein Read sieht das.
Warum ablehnen statt nur markieren: Die Alternative erzeugt eine "meine Fristen"-Liste, deren Zeilen für die zuständige Person 404 liefern. Eine abgelehnte Eingabe ist ein sichtbarer Fehler mit genanntem Grund. Eine Liste, die man nicht öffnen kann, ist ein kaputtes Produkt, das dem Nutzer die Schuld gibt.
Gleiche Form wie deine Eskalations-Entscheidung vom selben Tag (findings-rls-s6 §9c): benachrichtigt werden und öffnen können sind zwei verschiedene Berechtigungen.
Wenn du es anders willst: ein Rückgabewert in
validateAssignee(internal/services/assignee.go) schaltet die ganze Politik auf nur-markieren um. Sonst ändert sich nichts — die Read-Markierung ist schon da.Drei Fälle, die die Regel nicht offensichtlich abdeckt
project_id IS NULL): nur der Ersteller kann zuständig sein. Alles andere wird abgelehnt.false. "Niemand ist zuständig" und "die zuständige Person hat keinen Zugriff" sind verschiedene Aussagen. Die UI warnt nur bei einer davon."Meine Fristen" heisst
assigned_to = ichBewusst nicht "mir zugewiesen, oder von mir angelegt wenn niemand zugewiesen ist". Diese zweite Lesart lässt sich nicht in einem Satz erklären. "Erstellt von mir" gibt es schon als eigene Achse (
personal_only, Chip "Nur persönliche"), und beide lassen sich kombinieren. Eine PA, die die Fristen eines Partners einträgt, braucht genau diese Trennung — sie zusammenzuwerfen wäre dieselbe Verwechslung wiecreated_by, nur an neuer Stelle.Drei Modi:
me,unassigned,user.unassignedist der Triage-Blick, den ein Team braucht, um Fristen zu finden, die niemand übernommen hat.Wo die Lesart jetzt existiert
/deadlines,/appointments,/events?assignee=me|unassigned|user(+assignee_id)/events/summary/inbox,/agendaFilterSpec.Scope.Assignee, auf beiden Schienenassignee, zwei ChipsUI
Zuständigkeits-Feld auf allen vier Formularen (Frist neu/Detail, Termin neu/Detail). Die Auswahl zeigt das Team der Akte — das ist die Bequemlichkeit, damit der Normalfall nie in die 422 läuft. Der Server bleibt die Instanz.
Der PATCH auf der Fristen-Detailseite hat Fehler bisher verschluckt. Er sagt jetzt, warum ein Speichern abgelehnt wurde.
Erweiterung über den Issue-Scope hinaus (leicht zurückzunehmen)
Reminder.
assigned_tosteht jetzt nebencreated_byin jedem Zweig der Digest-Empfängerregeln. Eine Zuständigkeit, die die Person nicht per Mail erreicht, ist Dekoration. Beide bleiben Empfänger.Eigener Commit (97375ce), falls du das nicht willst. Es verbreitert das Empfänger-Prädikat genauso wie
escalation_contact_ides schon tut (findings-rls-s6 §9c) — bitte zusammen mit dem Abschnitt lesen, nicht getrennt.Nicht enthalten
ScopeSpec.Assigneeist die Einschränkung, auf die es gewartet hat.created_byabzuleiten würde genau die falsche Antwort erzeugen, die dieser Issue beheben soll.Vorgefundener Defekt, hier nicht behoben
deadline_project_changedundappointment_project_changedwerden von den Services geschrieben, fehlen aber inKnownProjectEventKinds— die Filter-UI kann sie nicht auswählen. Je eine Zeile. Gehört nicht zu einer Zuständigkeits-Änderung.Geprüft
TestDeadlineAssignee_Livegrün: Ablehnung beim Schreiben, Drift beim Lesen, "meine" = zuständig statt eingetragen, Verlauf-Eintrag beim Wechsel.scripts/ci-test-gate.shgrün (0 known-failing toleriert, keine neuen Fehler),make check-gofmtsauber, 411 Frontend-Tests grün.origin/maingemergt.Dokument:
docs/design-assignee-visibility-2026-07-30.md