Gedankenstriche durch Bindestriche ersetzt
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s

375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
This commit is contained in:
Harlekin
2026-09-04 08:54:52 +02:00
parent 38be773636
commit e0b03ea336
210 changed files with 368 additions and 368 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# AfiS-Aktensystem — Vorlage für den .env-Reiter in Dockge. # AfiS-Aktensystem - Vorlage für den .env-Reiter in Dockge.
# Diese Datei enthält nur Platzhalter. Die echten Werte gehören nach # Diese Datei enthält nur Platzhalter. Die echten Werte gehören nach
# /opt/stacks/afis/.env und niemals ins Repository. Siehe docs/11-betrieb.md. # /opt/stacks/afis/.env und niemals ins Repository. Siehe docs/11-betrieb.md.
+3 -3
View File
@@ -22,14 +22,14 @@ gehostet. Keine Mandantentrennung, keine öffentliche Registrierung.
## Zusicherungen ## Zusicherungen
Acht Regeln, die im Code und wo möglich in der Datenbank durchgesetzt sind. Sie sind der Acht Regeln, die im Code und wo möglich in der Datenbank durchgesetzt sind. Sie sind der
Grund, warum das System so gebaut ist, wie es gebaut ist — Codekommentare verweisen unter Grund, warum das System so gebaut ist, wie es gebaut ist - Codekommentare verweisen unter
ihrer Nummer hierher. ihrer Nummer hierher.
1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals 1. **Berechtigungsprüfung immer serverseitig und immer in der Datenbankabfrage.** Niemals
alle Datensätze laden und danach filtern. alle Datensätze laden und danach filtern.
2. **Verlaufseinträge dürfen nachträglich geändert werden, aber niemals unsichtbar.** Jede 2. **Verlaufseinträge dürfen nachträglich geändert werden, aber niemals unsichtbar.** Jede
Änderung erzeugt eine Version, jede Zurücknahme lässt einen Grabstein stehen, beides ist Änderung erzeugt eine Version, jede Zurücknahme lässt einen Grabstein stehen, beides ist
für alle sichtbar, die den Eintrag sehen dürfen. Es gibt kein `DELETE` — das steht als für alle sichtbar, die den Eintrag sehen dürfen. Es gibt kein `DELETE` - das steht als
Regel in der Datenbank, nicht nur in der Anwendung. Regel in der Datenbank, nicht nur in der Anwendung.
3. **Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen 3. **Jede schreibende Aktion und jeder Lesezugriff ab Stufe 3 erzeugt einen
Protokolleintrag.** Protokolleintrag.**
@@ -64,7 +64,7 @@ Container.
## Einrichten ## Einrichten
Discord-Application anlegen, Bot auf den Server einladen und **Server Members Intent Discord-Application anlegen, Bot auf den Server einladen und **Server Members Intent
aktivieren** — ohne das liefert der Bot keine Rollen. aktivieren** - ohne das liefert der Bot keine Rollen.
## Lokal entwickeln ## Lokal entwickeln
+1 -1
View File
@@ -1,4 +1,4 @@
# AfiS-Aktensystem — ein einziger Dockge-Stack. # AfiS-Aktensystem - ein einziger Dockge-Stack.
# Ablage: /opt/stacks/afis/compose.yaml, .env im eigenen Dockge-Reiter. # Ablage: /opt/stacks/afis/compose.yaml, .env im eigenen Dockge-Reiter.
# Begründung der Entscheidungen in docs/11-betrieb.md: # Begründung der Entscheidungen in docs/11-betrieb.md:
# - keine Einmal- und keine Init-Container, Dockge zeigt beendete Container nicht # - keine Einmal- und keine Init-Container, Dockge zeigt beendete Container nicht
@@ -211,7 +211,7 @@ $$ LANGUAGE sql STABLE;
-- Wie bei case_refresh_search_vector: mit IF verzweigt, nicht mit einem -- Wie bei case_refresh_search_vector: mit IF verzweigt, nicht mit einem
-- CASE-Ausdruck. Der würde auch im nicht genommenen Zweig verlangen, dass -- CASE-Ausdruck. Der würde auch im nicht genommenen Zweig verlangen, dass
-- NEW."documentId" existiert — auf "Document" gibt es das Feld nicht. -- NEW."documentId" existiert - auf "Document" gibt es das Feld nicht.
CREATE OR REPLACE FUNCTION document_refresh_search_vector() RETURNS trigger AS $$ CREATE OR REPLACE FUNCTION document_refresh_search_vector() RETURNS trigger AS $$
DECLARE DECLARE
target text; target text;
@@ -41,7 +41,7 @@ ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_userId_fkey"
-- Schwärzungen stufenweise auflösen -- Schwärzungen stufenweise auflösen
-- --
-- `strip_redactions` entfernt jede markierte Stelle. Für den Vektor einer -- `strip_redactions` entfernt jede markierte Stelle. Für den Vektor einer
-- Stufe muss aber stehen bleiben, was diese Stufe lesen darf — sonst findet -- Stufe muss aber stehen bleiben, was diese Stufe lesen darf - sonst findet
-- ein Leser der Stufe 4 einen Namen nicht, den er auf der Seite sieht. -- ein Leser der Stufe 4 einen Namen nicht, den er auf der Seite sieht.
-- =========================================================================== -- ===========================================================================
@@ -1,6 +1,6 @@
-- Phase 9: Ladewerte messen. -- Phase 9: Ladewerte messen.
-- --
-- Nur Zahlen und das Routenmuster. Keine Kennungen, keine konkreten Pfade — -- Nur Zahlen und das Routenmuster. Keine Kennungen, keine konkreten Pfade -
-- die Messung soll sagen, ob die Anwendung schnell ist, nicht wer wann welche -- die Messung soll sagen, ob die Anwendung schnell ist, nicht wer wann welche
-- Akte geöffnet hat. Siehe docs/09-performance-und-ux.md. -- Akte geöffnet hat. Siehe docs/09-performance-und-ux.md.
@@ -2,7 +2,7 @@
-- --
-- Eine persönliche Sprungliste, kein Verlauf: je Nutzer und Datensatz genau -- Eine persönliche Sprungliste, kein Verlauf: je Nutzer und Datensatz genau
-- eine Zeile, die beim nächsten Öffnen überschrieben wird. Der Primärschlüssel -- eine Zeile, die beim nächsten Öffnen überschrieben wird. Der Primärschlüssel
-- erzwingt das — es gibt keine Form, in der diese Tabelle sagen könnte, wie oft -- erzwingt das - es gibt keine Form, in der diese Tabelle sagen könnte, wie oft
-- oder in welcher Reihenfolge jemand etwas geöffnet hat. -- oder in welcher Reihenfolge jemand etwas geöffnet hat.
-- --
-- Wer wann was gelesen hat, beantwortet weiterhin allein das Zugriffsprotokoll -- Wer wann was gelesen hat, beantwortet weiterhin allein das Zugriffsprotokoll
@@ -9,7 +9,7 @@
-- Zeitpunkt dieser Migration stand in keinem Kanal eine Nachricht. -- Zeitpunkt dieser Migration stand in keinem Kanal eine Nachricht.
-- Die Nachrichtentabelle ist gegen DELETE gesperrt. Für das einmalige -- Die Nachrichtentabelle ist gegen DELETE gesperrt. Für das einmalige
-- Aufräumen wird die Regel abgenommen und danach wieder gesetzt — sie ist die -- Aufräumen wird die Regel abgenommen und danach wieder gesetzt - sie ist die
-- Zusicherung, dass niemand im Betrieb Nachrichten verschwinden lässt. -- Zusicherung, dass niemand im Betrieb Nachrichten verschwinden lässt.
DROP RULE chat_no_delete ON "ChatMessage"; DROP RULE chat_no_delete ON "ChatMessage";
@@ -2,7 +2,7 @@
-- --
-- Damit wird eine Lücke sichtbar, die es schon vorher gab: ein Beteiligter darf -- Damit wird eine Lücke sichtbar, die es schon vorher gab: ein Beteiligter darf
-- die Akte lesen, gleich in welcher Rolle. Bei „Betroffener" fiel das nicht auf, -- die Akte lesen, gleich in welcher Rolle. Bei „Betroffener" fiel das nicht auf,
-- bei „Täter" wäre es offensichtlich falsch — der Beschuldigte läse die -- bei „Täter" wäre es offensichtlich falsch - der Beschuldigte läse die
-- Ermittlung gegen sich selbst mit. -- Ermittlung gegen sich selbst mit.
-- --
-- Die Prüfung in lib/access/caseFilter.ts unterscheidet deshalb ab jetzt -- Die Prüfung in lib/access/caseFilter.ts unterscheidet deshalb ab jetzt
@@ -1,7 +1,7 @@
-- Abteilungen: die Ebene über der Personalakte. -- Abteilungen: die Ebene über der Personalakte.
-- --
-- Bisher stand die Abteilung als Freitext in der Personalakte. Damit liess sich -- Bisher stand die Abteilung als Freitext in der Personalakte. Damit liess sich
-- niemand fragen, was es zu einer Abteilung insgesamt gibt — jede Schreibweise -- niemand fragen, was es zu einer Abteilung insgesamt gibt - jede Schreibweise
-- war eine eigene Abteilung. Als eigenes Modell ist sie verwaltbar, und die -- war eine eigene Abteilung. Als eigenes Modell ist sie verwaltbar, und die
-- Zuordnung einer Fallakte ergibt sich aus ihren Beteiligten und deren -- Zuordnung einer Fallakte ergibt sich aus ihren Beteiligten und deren
-- Personalakten. Eine Fallakte trägt deshalb **keine** Abteilung: die -- Personalakten. Eine Fallakte trägt deshalb **keine** Abteilung: die
@@ -28,7 +28,7 @@ CREATE INDEX "Department_isArchived_name_idx" ON "Department"("isArchived", "nam
ALTER TABLE "PersonnelFile" ADD COLUMN "departmentId" TEXT; ALTER TABLE "PersonnelFile" ADD COLUMN "departmentId" TEXT;
-- Was schon als Freitext dastand, wird zur Abteilung. Der Schlüssel ist der -- Was schon als Freitext dastand, wird zur Abteilung. Der Schlüssel ist der
-- getrimmte Text in Grossbuchstaben, der Name der Text selbst — beides lässt -- getrimmte Text in Grossbuchstaben, der Name der Text selbst - beides lässt
-- sich im Adminbereich nachziehen. -- sich im Adminbereich nachziehen.
INSERT INTO "Department" ("id", "key", "name", "updatedAt") INSERT INTO "Department" ("id", "key", "name", "updatedAt")
SELECT gen_random_uuid()::text, SELECT gen_random_uuid()::text,
@@ -125,7 +125,7 @@ CREATE INDEX "PersonnelFile_departmentId_idx" ON "PersonnelFile"("departmentId")
-- Abteilung als dritter Besitzer wäre eine halbe Sache: die Vorlagen kennen -- Abteilung als dritter Besitzer wäre eine halbe Sache: die Vorlagen kennen
-- über TemplateUse nur die beiden Orte, und ein Feld, das nichts füllen kann, -- über TemplateUse nur die beiden Orte, und ein Feld, das nichts füllen kann,
-- ist schlimmer als keins. Was zu einer Abteilung an Dokumenten gehört, wird -- ist schlimmer als keins. Was zu einer Abteilung an Dokumenten gehört, wird
-- wie alles andere abgeleitet — über ihre Mitglieder und deren Fälle. -- wie alles andere abgeleitet - über ihre Mitglieder und deren Fälle.
-- Der Chat bekommt einen dritten Ort. Der CHECK aus der vorigen Migration wird -- Der Chat bekommt einen dritten Ort. Der CHECK aus der vorigen Migration wird
-- entsprechend erweitert: genau einer, weiterhin. -- entsprechend erweitert: genau einer, weiterhin.
@@ -161,7 +161,7 @@ $$ LANGUAGE plpgsql;
-- Dieselben Zusicherungen wie beim Verlauf einer Akte (harte Regel 2): kein -- Dieselben Zusicherungen wie beim Verlauf einer Akte (harte Regel 2): kein
-- DELETE, jede Änderung erzeugt eine Version, jede Zurücknahme lässt einen -- DELETE, jede Änderung erzeugt eine Version, jede Zurücknahme lässt einen
-- Grabstein stehen. Die Regeln stehen in der Datenbank und nicht nur in der -- Grabstein stehen. Die Regeln stehen in der Datenbank und nicht nur in der
-- Anwendung — die Anwendung kann man umgehen. -- Anwendung - die Anwendung kann man umgehen.
-- =========================================================================== -- ===========================================================================
CREATE TABLE "DepartmentEntry" ( CREATE TABLE "DepartmentEntry" (
@@ -216,7 +216,7 @@ CREATE RULE dept_entry_version_no_delete
AS ON DELETE TO "DepartmentEntryVersion" DO INSTEAD NOTHING; AS ON DELETE TO "DepartmentEntryVersion" DO INSTEAD NOTHING;
-- Autor, Entstehungszeit und Abteilung sind unantastbar. Wer etwas anderes -- Autor, Entstehungszeit und Abteilung sind unantastbar. Wer etwas anderes
-- sagen will, zieht zurück und schreibt neu — und genau das ist dann sichtbar. -- sagen will, zieht zurück und schreibt neu - und genau das ist dann sichtbar.
CREATE OR REPLACE FUNCTION dept_entry_immutable_guard() RETURNS trigger AS $$ CREATE OR REPLACE FUNCTION dept_entry_immutable_guard() RETURNS trigger AS $$
BEGIN BEGIN
IF NEW."authorId" IS DISTINCT FROM OLD."authorId" IF NEW."authorId" IS DISTINCT FROM OLD."authorId"
@@ -1,7 +1,7 @@
-- Beteiligte in den Suchindex der Fallakte. -- Beteiligte in den Suchindex der Fallakte.
-- --
-- Wer nach einem Namen sucht, will die Akten finden, in denen die Person -- Wer nach einem Namen sucht, will die Akten finden, in denen die Person
-- vorkommt — auch dann, wenn sie weder im Titel steht noch im Text erwähnt -- vorkommt - auch dann, wenn sie weder im Titel steht noch im Text erwähnt
-- wird, sondern nur als Zeuge oder Betroffener eingetragen ist. -- wird, sondern nur als Zeuge oder Betroffener eingetragen ist.
-- --
-- Das verrät nichts Neues: der Name eines Beteiligten steht ohnehin in der -- Das verrät nichts Neues: der Name eines Beteiligten steht ohnehin in der
@@ -74,7 +74,7 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- Der Verteiler kennt zwei neue Anlässe: eine geänderte Beteiligung, und einen -- Der Verteiler kennt zwei neue Anlässe: eine geänderte Beteiligung, und einen
-- geänderten Namen. Beim Namen sind alle Akten der Person betroffen — sonst -- geänderten Namen. Beim Namen sind alle Akten der Person betroffen - sonst
-- fände die Suche nach einer Umbenennung weiter den alten Namen. -- fände die Suche nach einer Umbenennung weiter den alten Namen.
CREATE OR REPLACE FUNCTION search_index_trigger() RETURNS trigger AS $$ CREATE OR REPLACE FUNCTION search_index_trigger() RETURNS trigger AS $$
DECLARE DECLARE
+12 -12
View File
@@ -1,4 +1,4 @@
// AfiS-Aktensystem — Datenmodell // AfiS-Aktensystem - Datenmodell
// Fachliche Grundlage: docs/04-datenmodell.md // Fachliche Grundlage: docs/04-datenmodell.md
// //
// Konventionen: // Konventionen:
@@ -113,7 +113,7 @@ model RoleMapping {
} }
enum RoleCategory { enum RoleCategory {
/// "AfiS", "Personalabteilung" — regelt, wer sich anmelden darf /// "AfiS", "Personalabteilung" - regelt, wer sich anmelden darf
MEMBERSHIP MEMBERSHIP
/// Dienstgrade, regeln die Rechte /// Dienstgrade, regeln die Rechte
RANK RANK
@@ -202,9 +202,9 @@ enum TemplateUse {
/// Bauform des Dokuments, siehe docs/13-entscheidungen.md. /// Bauform des Dokuments, siehe docs/13-entscheidungen.md.
enum DocumentLayout { enum DocumentLayout {
/// beschriftete Felder, ausgefuellt — der Vorfallsbericht /// beschriftete Felder, ausgefuellt - der Vorfallsbericht
FORM FORM
/// fester Wortlaut mit Platzhaltern, mehrseitig — der Sonderarbeitsvertrag /// fester Wortlaut mit Platzhaltern, mehrseitig - der Sonderarbeitsvertrag
PROSE PROSE
/// ein einzelnes Freitextfeld, dargestellt wie ein Vermerk /// ein einzelnes Freitextfeld, dargestellt wie ein Vermerk
NOTE NOTE
@@ -415,7 +415,7 @@ model CaseParticipant {
/// Zwei Gruppen, und der Unterschied entscheidet ueber den Zugriff: LEAD, /// Zwei Gruppen, und der Unterschied entscheidet ueber den Zugriff: LEAD,
/// INVESTIGATOR und OBSERVER **arbeiten** an der Akte und duerfen sie deshalb /// INVESTIGATOR und OBSERVER **arbeiten** an der Akte und duerfen sie deshalb
/// lesen. WITNESS, SUBJECT, SUSPECT und ACCOMPLICE sind Angaben **ueber** den /// lesen. WITNESS, SUBJECT, SUSPECT und ACCOMPLICE sind Angaben **ueber** den
/// Fall und verschaffen keinen Zugang — sonst laese der Beschuldigte die /// Fall und verschaffen keinen Zugang - sonst laese der Beschuldigte die
/// Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts. /// Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts.
enum ParticipantRole { enum ParticipantRole {
LEAD LEAD
@@ -467,8 +467,8 @@ enum LinkKind {
// Personalakten // Personalakten
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Akte zu einer Person. Ueberwiegend zu **Externen** — Sicherheitsdienst, /// Akte zu einer Person. Ueberwiegend zu **Externen** - Sicherheitsdienst,
/// Zeugen, Vertragspartner — die kein Discord-Konto haben. `userId` bleibt /// Zeugen, Vertragspartner - die kein Discord-Konto haben. `userId` bleibt
/// deshalb der Ausnahmefall, nicht die Regel. /// deshalb der Ausnahmefall, nicht die Regel.
model PersonnelFile { model PersonnelFile {
id String @id @default(uuid(7)) id String @id @default(uuid(7))
@@ -512,7 +512,7 @@ model PersonnelFile {
} }
/// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind /// Auswahl der Status-Etiketten, im Adminbereich pflegbar. Die Namen sind
/// Daten, keine Oberflaechentexte — siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch. /// Daten, keine Oberflaechentexte - siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
model PersonnelStatusTag { model PersonnelStatusTag {
key String @id // "tot", "gekuendigt" key String @id // "tot", "gekuendigt"
name String // "Tot", "Gekuendigt" name String // "Tot", "Gekuendigt"
@@ -554,7 +554,7 @@ enum AssignmentRole {
} }
/// Die Freigabestufen der Foundation, wie sie im Kopf eines Dokuments stehen. /// Die Freigabestufen der Foundation, wie sie im Kopf eines Dokuments stehen.
/// **Sie steuern keinen Zugriff** — das tut allein ClearanceLevelConfig. /// **Sie steuern keinen Zugriff** - das tut allein ClearanceLevelConfig.
/// Siehe docs/13-entscheidungen.md, "Zwei Freigaben im Kopf". /// Siehe docs/13-entscheidungen.md, "Zwei Freigaben im Kopf".
model FoundationClearance { model FoundationClearance {
level Int @id // 1..5 level Int @id // 1..5
@@ -849,7 +849,7 @@ model NotificationOutbox {
/// ///
/// Fast jede Person gehoert einer an, und die Abteilung ist die Ebene ueber /// Fast jede Person gehoert einer an, und die Abteilung ist die Ebene ueber
/// der Personalakte: was zu ihr aktenkundig ist, ergibt sich aus ihren /// der Personalakte: was zu ihr aktenkundig ist, ergibt sich aus ihren
/// Mitgliedern. Deshalb traegt eine Fallakte **keine** Abteilung — die /// Mitgliedern. Deshalb traegt eine Fallakte **keine** Abteilung - die
/// Zuordnung entsteht ueber die Beteiligten und deren Personalakten und ist /// Zuordnung entsteht ueber die Beteiligten und deren Personalakten und ist
/// damit immer aktuell. Siehe docs/13-entscheidungen.md. /// damit immer aktuell. Siehe docs/13-entscheidungen.md.
model Department { model Department {
@@ -881,7 +881,7 @@ model Department {
/// Dieselbe Zusicherung wie beim Verlauf einer Akte, in schmalerer Form: es /// Dieselbe Zusicherung wie beim Verlauf einer Akte, in schmalerer Form: es
/// gibt kein DELETE, jede Aenderung erzeugt eine Version, jede Zuruecknahme /// gibt kein DELETE, jede Aenderung erzeugt eine Version, jede Zuruecknahme
/// laesst einen Grabstein stehen. Was fehlt, sind die Teile, die nur eine /// laesst einen Grabstein stehen. Was fehlt, sind die Teile, die nur eine
/// Ermittlung braucht — Korrekturkette, Anhaenge, Karenzzeit. Ein /// Ermittlung braucht - Korrekturkette, Anhaenge, Karenzzeit. Ein
/// Abteilungsvermerk ist ein laufendes Protokoll, kein Beweismittel. /// Abteilungsvermerk ist ein laufendes Protokoll, kein Beweismittel.
model DepartmentEntry { model DepartmentEntry {
id String @id // clientseitig erzeugt, UUIDv7 id String @id // clientseitig erzeugt, UUIDv7
@@ -926,7 +926,7 @@ model DepartmentEntryVersion {
/// Der Vektor zur Stufe N enthaelt alles, was ein Leser dieser Stufe sehen /// Der Vektor zur Stufe N enthaelt alles, was ein Leser dieser Stufe sehen
/// darf: Kopfdaten, Abschnitte und Eintraege bis einschliesslich N, und aus /// darf: Kopfdaten, Abschnitte und Eintraege bis einschliesslich N, und aus
/// dem Fliesstext die geschwaerzten Stellen bis N. Eine Suche nimmt genau /// dem Fliesstext die geschwaerzten Stellen bis N. Eine Suche nimmt genau
/// eine Zeile — die zur Stufe des Suchenden — und kann deshalb nichts finden, /// eine Zeile - die zur Stufe des Suchenden - und kann deshalb nichts finden,
/// was er nicht lesen darf. /// was er nicht lesen darf.
/// ///
/// Der Vorgaenger war ein einziger Vektor mit nur dem, was jeder sehen darf. /// Der Vorgaenger war ein einziger Vektor mit nur dem, was jeder sehen darf.
+4 -4
View File
@@ -29,7 +29,7 @@ const CLEARANCE_LEVELS = [
/** /**
* Startvorlagen nach docs/05-akten-und-vorlagen.md. Die Feldschlüssel sind * Startvorlagen nach docs/05-akten-und-vorlagen.md. Die Feldschlüssel sind
* stabil und werden nie geändert — eine Akte merkt sich ihre Vorlage über * stabil und werden nie geändert - eine Akte merkt sich ihre Vorlage über
* `templateId`, und eine neue Vorlagenversion lässt bestehende Akten unberührt. * `templateId`, und eine neue Vorlagenversion lässt bestehende Akten unberührt.
* *
* Der Zuschnitt der Felder ist bewusst schmal gehalten: den Vorlagen-Editor * Der Zuschnitt der Felder ist bewusst schmal gehalten: den Vorlagen-Editor
@@ -75,7 +75,7 @@ const TEMPLATES: Vorlage[] = [
const TEMPLATE_FIELDS = [ const TEMPLATE_FIELDS = [
// Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe // Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe
// steht im Kopf des Dokuments, steuert aber keinen Zugriff — das tut allein // steht im Kopf des Dokuments, steuert aber keinen Zugriff - das tut allein
// die interne Freigabe. Siehe docs/13-entscheidungen.md. // die interne Freigabe. Siehe docs/13-entscheidungen.md.
{ {
key: 'foundation_freigabe', key: 'foundation_freigabe',
@@ -200,7 +200,7 @@ const PERSONALAKTE_FIELDS = [
/** /**
* Dokumentarten. Sie hängen an einer Fallakte, an einer Personalakte oder an * Dokumentarten. Sie hängen an einer Fallakte, an einer Personalakte oder an
* nichts — was davon, steht in `uses`. Eine weitere Art anzulegen ist ein * nichts - was davon, steht in `uses`. Eine weitere Art anzulegen ist ein
* Datensatz, kein Deployment: der Vorlagen-Editor reicht. * Datensatz, kein Deployment: der Vorlagen-Editor reicht.
* Siehe docs/13-entscheidungen.md. * Siehe docs/13-entscheidungen.md.
*/ */
@@ -367,7 +367,7 @@ async function main(): Promise<void> {
isActive: true, isActive: true,
}, },
// Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert // Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert
// der Leiter eine Vorlage im Editor, entsteht Version 2 — die fasst der // der Leiter eine Vorlage im Editor, entsteht Version 2 - die fasst der
// Seed nie an, und bestehende Akten behalten ohnehin ihre Version. // Seed nie an, und bestehende Akten behalten ohnehin ihre Version.
update: { update: {
name: template.name, name: template.name,
+3 -3
View File
@@ -10,7 +10,7 @@ import { resolveTestDatabaseUrl } from '../dist/testDatabase.js'
* Wird von `pnpm test` vorgeschaltet. * Wird von `pnpm test` vorgeschaltet.
* *
* **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und * **Neu aufsetzen, nicht nur anlegen.** Verlaufseinträge, Akten und
* Protokollzeilen sind nach Zusicherung 2 der README nicht löschbar — was * Protokollzeilen sind nach Zusicherung 2 der README nicht löschbar - was
* ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen * ein Testlauf anlegt, bleibt für immer stehen. Nach einigen Dutzend Läufen
* standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge * standen 2487 Akten in der Datenbank, und Tests, die eine auf zehn Einträge
* begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche * begrenzte Liste prüfen, fanden ihre eigene Akte nicht mehr darin. Solche
@@ -45,7 +45,7 @@ try {
const behalten = process.env.KEEP_TEST_DB === '1' const behalten = process.env.KEEP_TEST_DB === '1'
if (rowCount > 0 && !behalten) { if (rowCount > 0 && !behalten) {
// Offene Verbindungen verhindern das Verwerfen — etwa ein Prisma Studio // Offene Verbindungen verhindern das Verwerfen - etwa ein Prisma Studio
// oder ein abgebrochener Testlauf. // oder ein abgebrochener Testlauf.
await client.query( await client.query(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity `SELECT pg_terminate_backend(pid) FROM pg_stat_activity
@@ -87,7 +87,7 @@ execFileSync(process.execPath, [tsxCli, 'prisma/seed.ts'], {
// Die Änderungsrichtlinie ist eine einzige Zeile für die ganze Anwendung. Der // Die Änderungsrichtlinie ist eine einzige Zeile für die ganze Anwendung. Der
// Seed lässt sie bewusst in Ruhe, damit er die Einstellung des Leiters nicht // Seed lässt sie bewusst in Ruhe, damit er die Einstellung des Leiters nicht
// überschreibt — in der Testdatenbank soll sie aber bei jedem Lauf im // überschreibt - in der Testdatenbank soll sie aber bei jedem Lauf im
// Auslieferungszustand starten, sonst hängt das Ergebnis davon ab, was der // Auslieferungszustand starten, sonst hängt das Ergebnis davon ab, was der
// vorige Lauf hinterlassen hat. // vorige Lauf hinterlassen hat.
const reset = new pg.Client({ connectionString: testUrl }) const reset = new pg.Client({ connectionString: testUrl })
+1 -1
View File
@@ -33,7 +33,7 @@ export function resolveTestDatabaseUrl(source = process.env.DATABASE_URL): strin
/** /**
* Setzt die Umgebung eines Testlaufs auf die Testdatenbank um. Muss laufen, * Setzt die Umgebung eines Testlaufs auf die Testdatenbank um. Muss laufen,
* bevor irgendein Modul einen Prisma-Client baut — deshalb gehört der Aufruf * bevor irgendein Modul einen Prisma-Client baut - deshalb gehört der Aufruf
* in die `setupFiles` von Vitest, nicht in eine einzelne Testdatei. * in die `setupFiles` von Vitest, nicht in eine einzelne Testdatei.
*/ */
export function switchToTestDatabase(): string { export function switchToTestDatabase(): string {
@@ -48,7 +48,7 @@ suite('Auslieferungszustand', () => {
// Der Seed vergibt keine Rechte. Rollen kommen ausschliesslich aus dem // Der Seed vergibt keine Rechte. Rollen kommen ausschliesslich aus dem
// Abgleich des Workers und werden im Adminbereich zugeordnet. // Abgleich des Workers und werden im Adminbereich zugeordnet.
// Geprüft wird deshalb, dass der Seed nichts hinterlässt, das Rechte trägt // Geprüft wird deshalb, dass der Seed nichts hinterlässt, das Rechte trägt
// — nicht, dass die Tabelle leer ist: der Worker füllt sie im Betrieb. // - nicht, dass die Tabelle leer ist: der Worker füllt sie im Betrieb.
const mitRechten = await db.roleMapping.findMany({ const mitRechten = await db.roleMapping.findMany({
where: { permissions: { isEmpty: false } }, where: { permissions: { isEmpty: false } },
select: { label: true, permissions: true }, select: { label: true, permissions: true },
+1 -1
View File
@@ -240,7 +240,7 @@ suite('Datenbankregeln', () => {
}) })
it('löst Schwärzungen im Suchindex stufenweise auf', async () => { it('löst Schwärzungen im Suchindex stufenweise auf', async () => {
// Der Vektor zur Stufe N muss enthalten, was diese Stufe lesen darf — und // Der Vektor zur Stufe N muss enthalten, was diese Stufe lesen darf - und
// nichts darüber. Sonst findet ein Leser der Stufe 4 einen Namen nicht, // nichts darüber. Sonst findet ein Leser der Stufe 4 einen Namen nicht,
// den er auf der Seite sieht, oder ein Leser der Stufe 1 einen, den er // den er auf der Seite sieht, oder ein Leser der Stufe 1 einen, den er
// dort nur als Balken sieht. // dort nur als Balken sieht.
+1 -1
View File
@@ -8,7 +8,7 @@ import path from 'node:path'
* Der Anlass war handfest: ein von Hand angelegter GIN-Index über den * Der Anlass war handfest: ein von Hand angelegter GIN-Index über den
* Suchvektor stand in der Migration, aber nicht in `schema.prisma`. Prisma * Suchvektor stand in der Migration, aber nicht in `schema.prisma`. Prisma
* hielt das für einen Rückstand und fragte bei jedem `migrate dev` nach einem * hielt das für einen Rückstand und fragte bei jedem `migrate dev` nach einem
* Namen für eine neue Migration — deren Inhalt gewesen wäre, **den Suchindex * Namen für eine neue Migration - deren Inhalt gewesen wäre, **den Suchindex
* zu löschen**. Wer die Frage in Eile beantwortet, verliert die gestufte * zu löschen**. Wer die Frage in Eile beantwortet, verliert die gestufte
* Volltextsuche und merkt es erst, wenn die Suche leer bleibt. * Volltextsuche und merkt es erst, wenn die Suche leer bleibt.
* *
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Spielt eine Sicherung in eine Wegwerf-Datenbank ein und prüft, ob alles # Spielt eine Sicherung in eine Wegwerf-Datenbank ein und prüft, ob alles
# angekommen ist. Ein ungetestetes Backup ist keins — siehe docs/11-betrieb.md. # angekommen ist. Ein ungetestetes Backup ist keins - siehe docs/11-betrieb.md.
# #
# scripts/restore-test.sh [container] [quelldatenbank] # scripts/restore-test.sh [container] [quelldatenbank]
# #
+1 -1
View File
@@ -98,7 +98,7 @@ mkdir -p "${FILES_DIR_LOKAL}/quarantine" "${FILES_DIR_LOKAL}/store"
info "Dateiablage unter ${FILES_DIR_LOKAL}" info "Dateiablage unter ${FILES_DIR_LOKAL}"
# Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in # Die Prüfkette braucht beides. Fehlt eines, bleiben die betroffenen Uploads in
# der Quarantäne — sie gehen nicht ungeprüft durch. Siehe docs/07. # der Quarantäne - sie gehen nicht ungeprüft durch. Siehe docs/07.
CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)" CLAMAV_SOCKET_LOKAL="$(grep -E '^CLAMAV_SOCKET=' .env 2>/dev/null | cut -d= -f2- || true)"
[ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \ [ -S "${CLAMAV_SOCKET_LOKAL:-/var/run/clamav/clamd.sock}" ] \
|| warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand" || warn "Kein clamd erreichbar: Uploads landen auf 'Prüfung unvollständig' und brauchen die Freigabe von Hand"
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -39,7 +39,7 @@ export async function generateMetadata({
* *
* Dasselbe Bild wie eine Personalakte, eine Stufe höher: wer gehört dazu, was * Dasselbe Bild wie eine Personalakte, eine Stufe höher: wer gehört dazu, was
* ist dazu aktenkundig, was ist dazu vermerkt, und worüber wird geredet. Der * ist dazu aktenkundig, was ist dazu vermerkt, und worüber wird geredet. Der
* Unterschied ist, dass hier **nichts eingetragen** wird — die Fälle ergeben * Unterschied ist, dass hier **nichts eingetragen** wird - die Fälle ergeben
* sich aus den Beteiligten und deren Personalakten. * sich aus den Beteiligten und deren Personalakten.
*/ */
export default async function DepartmentPage({ params }: { params: Promise<{ key: string }> }) { export default async function DepartmentPage({ params }: { params: Promise<{ key: string }> }) {
+1 -1
View File
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
* *
* Das Diagramm beantwortet die Frage, mit der man auf diese Seite kommt: zu * Das Diagramm beantwortet die Frage, mit der man auf diese Seite kommt: zu
* welcher Abteilung gibt es die meisten Fälle. Gezählt wird nur, was der * welcher Abteilung gibt es die meisten Fälle. Gezählt wird nur, was der
* Fragende auch sehen darf — eine Zahl, die mehr zählt als seine Liste * Fragende auch sehen darf - eine Zahl, die mehr zählt als seine Liste
* hergibt, wäre selbst eine Auskunft. * hergibt, wäre selbst eine Auskunft.
*/ */
export default async function DepartmentsPage() { export default async function DepartmentsPage() {
@@ -23,7 +23,7 @@ type Existing = {
* *
* Das Kürzel steht nach dem Anlegen fest: es steckt in der Adresse jeder * Das Kürzel steht nach dem Anlegen fest: es steckt in der Adresse jeder
* Abteilungsseite, und ein wandernder Bezeichner macht geteilte Links wertlos. * Abteilungsseite, und ein wandernder Bezeichner macht geteilte Links wertlos.
* Wer sich vertan hat, legt neu an und archiviert die alte — archivieren * Wer sich vertan hat, legt neu an und archiviert die alte - archivieren
* statt löschen, weil an einer Abteilung Vermerke und Zuordnungen hängen. * statt löschen, weil an einer Abteilung Vermerke und Zuordnungen hängen.
*/ */
export function DepartmentForm({ export function DepartmentForm({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic'
* *
* Wie bei der Fallakte sind die Bereiche **eigene Seiten** unter einem * Wie bei der Fallakte sind die Bereiche **eigene Seiten** unter einem
* gemeinsamen Layout, keine umgeschalteten Blöcke. Wer gar keinen Bereich * gemeinsamen Layout, keine umgeschalteten Blöcke. Wer gar keinen Bereich
* sehen darf, bekommt 404 — die Verwaltung ist für ihn nicht vorhanden. * sehen darf, bekommt 404 - die Verwaltung ist für ihn nicht vorhanden.
*/ */
export default async function AdminLayout({ children }: { children: ReactNode }) { export default async function AdminLayout({ children }: { children: ReactNode }) {
const user = await requireUser() const user = await requireUser()
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -11,7 +11,7 @@ export const dynamic = 'force-dynamic'
* Die Startseite der Verwaltung: was es gibt und wofür es da ist. * Die Startseite der Verwaltung: was es gibt und wofür es da ist.
* *
* Kein Weiterleiten auf den ersten Bereich. Wer die Verwaltung öffnet, weiss * Kein Weiterleiten auf den ersten Bereich. Wer die Verwaltung öffnet, weiss
* nicht immer, wo das Gesuchte liegt — eine Liste mit einem Satz je Bereich * nicht immer, wo das Gesuchte liegt - eine Liste mit einem Satz je Bereich
* beantwortet das schneller als das Durchklicken der Leiste. * beantwortet das schneller als das Durchklicken der Leiste.
*/ */
export default async function AdminIndexPage() { export default async function AdminIndexPage() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -18,7 +18,7 @@ export type PolicyResult = { ok: true } | { ok: false; error: string }
/** /**
* Die Änderungsrichtlinie gilt für die ganze Abteilung und wird serverseitig * Die Änderungsrichtlinie gilt für die ganze Abteilung und wird serverseitig
* gelesen, nie aus dem Request. Deshalb ist das hier die einzige Stelle, an * gelesen, nie aus dem Request. Deshalb ist das hier die einzige Stelle, an
* der sie sich ändern lässt — mit Recht `admin` und mit Protokolleintrag. * der sie sich ändern lässt - mit Recht `admin` und mit Protokolleintrag.
*/ */
export async function savePolicy(input: unknown): Promise<PolicyResult> { export async function savePolicy(input: unknown): Promise<PolicyResult> {
const user = await requirePermissionForAction('admin') const user = await requirePermissionForAction('admin')
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -10,7 +10,7 @@ export const dynamic = 'force-dynamic'
/** /**
* Reihenfolge der Abschnitte: erst die Ränge, dann die Zugehörigkeit, dann die * Reihenfolge der Abschnitte: erst die Ränge, dann die Zugehörigkeit, dann die
* Zugangsrollen — so, wie die Abteilung sie selbst aufzählt. Innerhalb eines * Zugangsrollen - so, wie die Abteilung sie selbst aufzählt. Innerhalb eines
* Abschnitts entscheidet die Position der Rolle auf dem Discord-Server, oben * Abschnitts entscheidet die Position der Rolle auf dem Discord-Server, oben
* die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt * die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt
* wird, statt in einer zweiten Sortierung in dieser Anwendung. * wird, statt in einer zweiten Sortierung in dieser Anwendung.
+1 -1
View File
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
* Die Bereiche der Verwaltung, an einer Stelle. * Die Bereiche der Verwaltung, an einer Stelle.
* *
* Vorher standen Rollenverwaltung, Vorlagen und Änderungsrichtlinie als drei * Vorher standen Rollenverwaltung, Vorlagen und Änderungsrichtlinie als drei
* eigene Einträge in der Kopfzeile — drei Einträge für etwas, das man selten * eigene Einträge in der Kopfzeile - drei Einträge für etwas, das man selten
* und dann meist zusammenhängend braucht. Jetzt ist es ein Eintrag mit einer * und dann meist zusammenhängend braucht. Jetzt ist es ein Eintrag mit einer
* Leiste darunter. * Leiste darunter.
* *
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -97,7 +97,7 @@ export default async function SystemPage() {
/** /**
* Die Ladewerte der letzten sieben Tage. Median und 75. Perzentil, kein * Die Ladewerte der letzten sieben Tage. Median und 75. Perzentil, kein
* Mittelwert — ein Ausreisser auf einem alten Laptop soll die Zahl nicht * Mittelwert - ein Ausreisser auf einem alten Laptop soll die Zahl nicht
* bestimmen. * bestimmen.
*/ */
async function VitalsTable() { async function VitalsTable() {
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -13,7 +13,7 @@ const schema = z.object({ id: z.string().min(1) })
/** /**
* Freigabe einer Datei, deren Prüfung nicht abgeschlossen werden konnte. * Freigabe einer Datei, deren Prüfung nicht abgeschlossen werden konnte.
* Nur mit Recht `admin`, geprüft im Dienst — nicht hier. * Nur mit Recht `admin`, geprüft im Dienst - nicht hier.
*/ */
export async function releaseAttachmentAction(input: unknown): Promise<ActionResult> { export async function releaseAttachmentAction(input: unknown): Promise<ActionResult> {
const user = await requireUser() const user = await requireUser()
@@ -14,7 +14,7 @@ import {
* Ausfertigen, Zurückziehen, Umklassifizieren. * Ausfertigen, Zurückziehen, Umklassifizieren.
* *
* Wie beim Fall bewusst **nicht** optimistisch: alle drei sind folgenreich. * Wie beim Fall bewusst **nicht** optimistisch: alle drei sind folgenreich.
* Das Ausfertigen fragt nach, das Zurückziehen verlangt eine Begründung — es * Das Ausfertigen fragt nach, das Zurückziehen verlangt eine Begründung - es
* gibt kein Löschen, der Grabstein bleibt stehen. * gibt kein Löschen, der Grabstein bleibt stehen.
*/ */
export function DocumentActions({ export function DocumentActions({
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -10,7 +10,7 @@ export type ActionResult = { ok: true } | { ok: false; error: string }
const schema = z.object({ notifyMode: z.enum(['ALL', 'OWN_CASES', 'MENTIONS', 'OFF']) }) const schema = z.object({ notifyMode: z.enum(['ALL', 'OWN_CASES', 'MENTIONS', 'OFF']) })
/** Jeder stellt nur für sich selbst ein — die Kennung kommt aus der Sitzung. */ /** Jeder stellt nur für sich selbst ein - die Kennung kommt aus der Sitzung. */
export async function saveNotifyModeAction( export async function saveNotifyModeAction(
_prev: ActionResult | null, _prev: ActionResult | null,
formData: FormData, formData: FormData,
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -28,7 +28,7 @@ const requestSchema = z.union([
/** /**
* Export anfordern. * Export anfordern.
* *
* Der Auftrag läuft im Hintergrund weiter, nachdem die Antwort raus ist — das * Der Auftrag läuft im Hintergrund weiter, nachdem die Antwort raus ist - das
* Rendern dauert, und niemand soll auf einen Fortschrittsbalken starren, der * Rendern dauert, und niemand soll auf einen Fortschrittsbalken starren, der
* auf Playwright wartet. Siehe docs/09-performance-und-ux.md. * auf Playwright wartet. Siehe docs/09-performance-und-ux.md.
*/ */
@@ -90,7 +90,7 @@ export function CaseActions({
</button> </button>
</div> </div>
{/* Welche Zustände von hier aus erreichbar sind, ergibt sich aus dem {/* Welche Zustände von hier aus erreichbar sind, ergibt sich aus dem
Lebenszyklus — aus einem Entwurf führt nur „Offen" heraus. Ohne Lebenszyklus - aus einem Entwurf führt nur „Offen" heraus. Ohne
diesen Satz sieht die kurze Auswahl aus wie ein Fehler. */} diesen Satz sieht die kurze Auswahl aus wie ein Fehler. */}
<p className="mt-1.5 max-w-sm text-2xs text-text-dim"> <p className="mt-1.5 max-w-sm text-2xs text-text-dim">
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint} {caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
@@ -113,7 +113,7 @@ export function Participants({
))} ))}
</PanelSelect> </PanelSelect>
<PanelSelect value={target} onChange={setTarget} label={participantText.person}> <PanelSelect value={target} onChange={setTarget} label={participantText.person}>
<option value="">—</option> <option value="">-</option>
<optgroup label={participantText.pickUser}> <optgroup label={participantText.pickUser}>
{members.map((member) => ( {members.map((member) => (
<option key={member.id} value={`user:${member.id}`}> <option key={member.id} value={`user:${member.id}`}>
@@ -7,7 +7,7 @@ import { toggleWatchAction } from '../actions'
/** /**
* Akte beobachten. * Akte beobachten.
* *
* Sofortiges Umschalten, der Server bestätigt nur — misslingt es, springt der * Sofortiges Umschalten, der Server bestätigt nur - misslingt es, springt der
* Zustand zurück. Beobachten ist umkehrbar und harmlos, dafür braucht es * Zustand zurück. Beobachten ist umkehrbar und harmlos, dafür braucht es
* keinen Wartezustand. Siehe docs/09-performance-und-ux.md. * keinen Wartezustand. Siehe docs/09-performance-und-ux.md.
*/ */
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
@@ -15,7 +15,7 @@ export const dynamic = 'force-dynamic'
* *
* Für die Abteilungsleitung ist das die Ansicht, mit der sie im Zweifel prüft, * Für die Abteilungsleitung ist das die Ansicht, mit der sie im Zweifel prüft,
* ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle, die die Akte * ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle, die die Akte
* lesen dürfen — nicht nur für die Leitung. Das ist der ganze Sinn der Sache. * lesen dürfen - nicht nur für die Leitung. Das ist der ganze Sinn der Sache.
*/ */
export default async function ChangesPage({ export default async function ChangesPage({
params, params,
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+2 -2
View File
@@ -20,12 +20,12 @@ export const dynamic = 'force-dynamic'
* Die Akte ist in Reiter geteilt, und zwar in **eigene Seiten**, nicht in * Die Akte ist in Reiter geteilt, und zwar in **eigene Seiten**, nicht in
* umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte * umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte
* war die dichteste Seite der Anwendung und hatte das Bündelbudget aus * war die dichteste Seite der Anwendung und hatte das Bündelbudget aus
* docs/09-performance-und-ux.md aufgebraucht — Beteiligte, Verknüpfungen, * docs/09-performance-und-ux.md aufgebraucht - Beteiligte, Verknüpfungen,
* Dokumente und Anhänge lagen mit auf der Seite, obwohl man sie selten * Dokumente und Anhänge lagen mit auf der Seite, obwohl man sie selten
* gleichzeitig braucht. * gleichzeitig braucht.
* *
* Fachlich passt der Schnitt ohnehin: die drei Ebenen aus * Fachlich passt der Schnitt ohnehin: die drei Ebenen aus
* docs/05-akten-und-vorlagen.md — Kopfdaten, Abschnitte, Verlauf — bleiben * docs/05-akten-und-vorlagen.md - Kopfdaten, Abschnitte, Verlauf - bleiben
* zusammen, das Umfeld der Akte bekommt seinen eigenen Platz. * zusammen, das Umfeld der Akte bekommt seinen eigenen Platz.
*/ */
export default async function CaseLayout({ export default async function CaseLayout({
+1 -1
View File
@@ -185,7 +185,7 @@ async function ChatBlock({
/** /**
* Der Reiter „Akte": die drei Ebenen aus docs/05-akten-und-vorlagen.md, die * Der Reiter „Akte": die drei Ebenen aus docs/05-akten-und-vorlagen.md, die
* zusammengehören — Abschnitte als Sachstand, Verlauf als Chronologie, dazu * zusammengehören - Abschnitte als Sachstand, Verlauf als Chronologie, dazu
* der formlose Aktenchat. Kopfdaten und Reiterleiste stehen im Layout. * der formlose Aktenchat. Kopfdaten und Reiterleiste stehen im Layout.
*/ */
export default async function CasePage({ export default async function CasePage({
@@ -51,7 +51,7 @@ export async function saveSectionAction(
/** /**
* Mehrere Abschnitte in einem Zug. * Mehrere Abschnitte in einem Zug.
* *
* Jedes Feld wird einzeln geschrieben und einzeln beantwortet — Abschnitte * Jedes Feld wird einzeln geschrieben und einzeln beantwortet - Abschnitte
* sind einzeln versioniert, und ein zwischenzeitlich geändertes Feld soll die * sind einzeln versioniert, und ein zwischenzeitlich geändertes Feld soll die
* übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger * übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger
* fremder Eingriff das ganze Blatt. * fremder Eingriff das ganze Blatt.
+2 -2
View File
@@ -66,7 +66,7 @@ export async function createCaseAction(
caseNumber = created.caseNumber caseNumber = created.caseNumber
// Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten // Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten
// Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem — sie // Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem - sie
// ist das Wichtigere, und nachtragen kann man auf der Aktenseite. // ist das Wichtigere, und nachtragen kann man auf der Aktenseite.
for (const eintrag of beteiligte) { for (const eintrag of beteiligte) {
await addParticipant(user.permissions, { await addParticipant(user.permissions, {
@@ -294,7 +294,7 @@ const watchSchema = z.object({ caseNumber: z.string().min(1) })
export type WatchResult = { ok: true; watching: boolean } | { ok: false; error: string } export type WatchResult = { ok: true; watching: boolean } | { ok: false; error: string }
/** Beobachten verschafft keinen Zugriff — nur was sichtbar ist, lässt sich merken. */ /** Beobachten verschafft keinen Zugriff - nur was sichtbar ist, lässt sich merken. */
export async function toggleWatchAction(input: unknown): Promise<WatchResult> { export async function toggleWatchAction(input: unknown): Promise<WatchResult> {
const user = await requireUser() const user = await requireUser()
const parsed = watchSchema.safeParse(input) const parsed = watchSchema.safeParse(input)
+2 -2
View File
@@ -25,7 +25,7 @@ export function NewCaseForm({
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {}) const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
// Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau // Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau
// eine „betroffene Person" — wer einen Zeugen und einen Täter eintragen // eine „betroffene Person" - wer einen Zeugen und einen Täter eintragen
// wollte, musste die Akte erst anlegen und dann noch einmal hinein. // wollte, musste die Akte erst anlegen und dann noch einmal hinein.
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([]) const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
const [naechster, setNaechster] = useState(1) const [naechster, setNaechster] = useState(1)
@@ -133,7 +133,7 @@ export function NewCaseForm({
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })} onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
className="field grow" className="field grow"
> >
<option value="">—</option> <option value="">-</option>
<optgroup label={participantText.pickUser}> <optgroup label={participantText.pickUser}>
{members.map((member) => ( {members.map((member) => (
<option key={member.id} value={`user:${member.id}`}> <option key={member.id} value={`user:${member.id}`}>
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -82,7 +82,7 @@ export default async function CaseListPage({
</Link> </Link>
{/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten, {/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten,
und das waren ausgerechnet die beiden, die in der Liste am häufigsten und das waren ausgerechnet die beiden, die in der Liste am häufigsten
standen — man konnte nach dem filtern, was man sah, gerade nicht. standen - man konnte nach dem filtern, was man sah, gerade nicht.
Versiegelt nur für den, der Versiegeltes überhaupt lesen darf. */} Versiegelt nur für den, der Versiegeltes überhaupt lesen darf. */}
{CASE_STATUSES.filter( {CASE_STATUSES.filter(
(value) => (value) =>
+2 -2
View File
@@ -13,8 +13,8 @@ import { hasAdminAccess } from './admin/sections'
/** /**
* Das Rahmenwerk der angemeldeten Anwendung. * Das Rahmenwerk der angemeldeten Anwendung.
* *
* Die Kopfzeile trägt links die Bereiche der Arbeit — Übersicht, Fallakten, * Die Kopfzeile trägt links die Bereiche der Arbeit - Übersicht, Fallakten,
* Personalakten, Verwaltung — und rechts alles, was den Nutzer selbst * Personalakten, Verwaltung - und rechts alles, was den Nutzer selbst
* betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden * betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden
* standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg, * standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg,
* den die Arbeit braucht; sie sitzen jetzt hinter dem Profilbild. * den die Arbeit braucht; sie sitzen jetzt hinter dem Profilbild.
@@ -17,7 +17,7 @@ type Zuordnung = {
* Mitglieder der Abteilung, die an dieser Personalakte hängen. * Mitglieder der Abteilung, die an dieser Personalakte hängen.
* *
* Wie die Beteiligten einer Fallakte ist das zugleich der Weg zum * Wie die Beteiligten einer Fallakte ist das zugleich der Weg zum
* ausnahmsweisen Zugang — der Hinweis steht deshalb direkt am Formular. * ausnahmsweisen Zugang - der Hinweis steht deshalb direkt am Formular.
*/ */
export function Assignments({ export function Assignments({
personnelFileId, personnelFileId,
@@ -94,7 +94,7 @@ export function Assignments({
))} ))}
</PanelSelect> </PanelSelect>
<PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}> <PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}>
<option value="">—</option> <option value="">-</option>
{members.map((member) => ( {members.map((member) => (
<option key={member.id} value={member.id}> <option key={member.id} value={member.id}>
{member.displayName} {member.displayName}
@@ -12,7 +12,7 @@ import { linkCaseToPersonAction } from '../actions'
* *
* Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der * Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der
* anderen Seite gedacht: dort sucht man die Person, hier den Fall. Deshalb ein * anderen Seite gedacht: dort sucht man die Person, hier den Fall. Deshalb ein
* Eingabefeld für das Aktenzeichen und keine Auswahlliste — eine Liste aller * Eingabefeld für das Aktenzeichen und keine Auswahlliste - eine Liste aller
* sichtbaren Fälle wäre auf einer Personalakte am falschen Ort. * sichtbaren Fälle wäre auf einer Personalakte am falschen Ort.
*/ */
export function LinkCase({ personnelFileId }: { personnelFileId: string }) { export function LinkCase({ personnelFileId }: { personnelFileId: string }) {
@@ -27,7 +27,7 @@ type File = {
* Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht. * Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht.
* *
* Die Klassifizierung steht bewusst getrennt. Sie ändert, wer die Akte * Die Klassifizierung steht bewusst getrennt. Sie ändert, wer die Akte
* überhaupt sieht, und eine Herabstufung braucht eine Begründung — das gehört * überhaupt sieht, und eine Herabstufung braucht eine Begründung - das gehört
* nicht in dasselbe Formular wie eine Korrektur der Dienstnummer. * nicht in dasselbe Formular wie eine Korrektur der Dienstnummer.
*/ */
export function PersonnelHead({ export function PersonnelHead({
@@ -276,7 +276,7 @@ function Zeile({
return ( return (
<div className="flex gap-3"> <div className="flex gap-3">
<dt className="font-condensed text-xs text-text-dim">{label}</dt> <dt className="font-condensed text-xs text-text-dim">{label}</dt>
<dd className={mono ? 'font-mono text-xs' : ''}>{value || '—'}</dd> <dd className={mono ? 'font-mono text-xs' : ''}>{value || '-'}</dd>
</div> </div>
) )
} }
+1 -1
View File
@@ -171,7 +171,7 @@ async function AssignmentBlock({
* dass das Gespräch bei ihrer Akte steht, war genau das, was vorher auf * dass das Gespräch bei ihrer Akte steht, war genau das, was vorher auf
* Discord passiert ist. Siehe docs/13-entscheidungen.md. * Discord passiert ist. Siehe docs/13-entscheidungen.md.
* *
* Übernehmen als Vermerk gibt es hier nicht — dafür braucht es einen Verlauf, * Übernehmen als Vermerk gibt es hier nicht - dafür braucht es einen Verlauf,
* und den führt nur die Fallakte. * und den führt nur die Fallakte.
*/ */
async function ChatBlock({ async function ChatBlock({
+1 -1
View File
@@ -71,7 +71,7 @@ export async function createPersonnelAction(
id = created.id id = created.id
// Das Aktenblatt ist der erste Eintrag zu einer Person und entsteht // Das Aktenblatt ist der erste Eintrag zu einer Person und entsteht
// deshalb gleich mit — als Entwurf, mit dem Namen schon eingetragen. // deshalb gleich mit - als Entwurf, mit dem Namen schon eingetragen.
// Fehlt das Recht dafür, bleibt es bei der Akte allein. // Fehlt das Recht dafür, bleibt es bei der Akte allein.
await erstesAktenblatt(user.permissions, id, parsed.data.displayName) await erstesAktenblatt(user.permissions, id, parsed.data.displayName)
} catch (error) { } catch (error) {
@@ -8,8 +8,8 @@ import { createPersonnelAction, type ActionResult } from '../actions'
/** /**
* Neue Personalakte. * Neue Personalakte.
* *
* Das Discord-Konto ist **optional**. Personen ohne Konto — Zeugen, ehemalige * Das Discord-Konto ist **optional**. Personen ohne Konto - Zeugen, ehemalige
* Mitarbeiter, Externe — brauchen genauso eine Akte, und ihr Name ist dann das * Mitarbeiter, Externe - brauchen genauso eine Akte, und ihr Name ist dann das
* einzige, was sie identifiziert. * einzige, was sie identifiziert.
*/ */
export function NewPersonnelForm({ export function NewPersonnelForm({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -22,7 +22,7 @@ export default async function PersonnelListPage({
}: { }: {
searchParams: Promise<{ q?: string; status?: string }> searchParams: Promise<{ q?: string; status?: string }>
}) { }) {
// Ohne das Recht gibt es die Seite nicht — 404, nicht 403. // Ohne das Recht gibt es die Seite nicht - 404, nicht 403.
const user = await requirePermission('personnel.read') const user = await requirePermission('personnel.read')
const { q, status } = await searchParams const { q, status } = await searchParams
+1 -1
View File
@@ -9,7 +9,7 @@ import { deleteViewAction, saveViewAction } from './actions'
/** /**
* Ansichten: die mitgelieferten und die eigenen. * Ansichten: die mitgelieferten und die eigenen.
* *
* Beide sind dasselbe — eine Suchadresse. Die mitgelieferten stehen im * Beide sind dasselbe - eine Suchadresse. Die mitgelieferten stehen im
* Programm, weil sie von der Gegenwart abhängen („diese Woche"), die eigenen * Programm, weil sie von der Gegenwart abhängen („diese Woche"), die eigenen
* in der Datenbank. * in der Datenbank.
*/ */
+1 -1
View File
@@ -10,7 +10,7 @@ const DEBOUNCE_MS = 250
/** /**
* Das Suchfeld. * Das Suchfeld.
* *
* Tippen schreibt in die Adresse, mit 250 ms Verzögerung — dadurch ist jeder * Tippen schreibt in die Adresse, mit 250 ms Verzögerung - dadurch ist jeder
* Zustand teilbar und der Zurück-Knopf tut das Richtige. Während die neue * Zustand teilbar und der Zurück-Knopf tut das Richtige. Während die neue
* Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu * Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu
* verschwinden. Siehe docs/09-performance-und-ux.md. * verschwinden. Siehe docs/09-performance-und-ux.md.
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+5 -5
View File
@@ -30,7 +30,7 @@ export const dynamic = 'force-dynamic'
* Suchseite. * Suchseite.
* *
* Alles läuft über Adressparameter: das macht jeden Zustand teilbar, den * Alles läuft über Adressparameter: das macht jeden Zustand teilbar, den
* Zurück-Knopf richtig und eine gespeicherte Ansicht zu dem, was sie ist — * Zurück-Knopf richtig und eine gespeicherte Ansicht zu dem, was sie ist -
* ein Lesezeichen. Siehe lib/search/query.ts. * ein Lesezeichen. Siehe lib/search/query.ts.
*/ */
export default async function SearchPage({ export default async function SearchPage({
@@ -76,7 +76,7 @@ export default async function SearchPage({
]) ])
// Bei einer einzelnen Art hängt alles Weitere an ihr: Facetten, Blättern, // Bei einer einzelnen Art hängt alles Weitere an ihr: Facetten, Blättern,
// die Trefferzahl in der Kopfzeile. Bei „Alle" gibt es das nicht — dort ist // die Trefferzahl in der Kopfzeile. Bei „Alle" gibt es das nicht - dort ist
// jede Art ein eigener Block mit eigenem Weg in die Vollansicht. // jede Art ein eigener Block mit eigenem Weg in die Vollansicht.
const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)! const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)!
const gesamt = alle const gesamt = alle
@@ -108,7 +108,7 @@ export default async function SearchPage({
{SEARCH_SCOPES.map((kind) => { {SEARCH_SCOPES.map((kind) => {
const anzahl = kind === 'all' ? gesamt : zahlen[kind] const anzahl = kind === 'all' ? gesamt : zahlen[kind]
// `null` heisst: diese Art gibt es für diesen Nutzer nicht. Dann // `null` heisst: diese Art gibt es für diesen Nutzer nicht. Dann
// fehlt der Reiter, statt eine Null zu zeigen — auch eine Null wäre // fehlt der Reiter, statt eine Null zu zeigen - auch eine Null wäre
// eine Auskunft. // eine Auskunft.
if (anzahl === null) return null if (anzahl === null) return null
@@ -279,7 +279,7 @@ type TrefferView =
* *
* Welche Art es ist, steht an der Form: ein Treffer mit Aktenzeichen ist eine * Welche Art es ist, steht an der Form: ein Treffer mit Aktenzeichen ist eine
* Fallakte. Das ist kein Trick, sondern der Grund, warum die drei Suchen * Fallakte. Das ist kein Trick, sondern der Grund, warum die drei Suchen
* getrennte Rückgaben haben — ein gemeinsamer Typ mit lauter optionalen * getrennte Rückgaben haben - ein gemeinsamer Typ mit lauter optionalen
* Feldern liesse offen, was wirklich dasteht. * Feldern liesse offen, was wirklich dasteht.
*/ */
function Treffer({ function Treffer({
@@ -371,7 +371,7 @@ async function listStatusTags() {
} }
/** /**
* Eine Facette. Die Zahlen stammen aus derselben Abfrage wie die Liste — ein * Eine Facette. Die Zahlen stammen aus derselben Abfrage wie die Liste - ein
* Zähler, der mehr zählt als die Liste zeigt, wäre selbst eine Information. * Zähler, der mehr zählt als die Liste zeigt, wäre selbst eine Information.
*/ */
function Facette({ function Facette({
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+3 -3
View File
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
* *
* Woran arbeite ich, was ist fällig, wo hat jemand geschrieben, was hat sich * Woran arbeite ich, was ist fällig, wo hat jemand geschrieben, was hat sich
* getan. Jede Liste läuft durch dieselbe Sichtbarkeitsprüfung wie die * getan. Jede Liste läuft durch dieselbe Sichtbarkeitsprüfung wie die
* Fallliste — das ist der Ort, an dem eine unachtsame Abfrage am ehesten eine * Fallliste - das ist der Ort, an dem eine unachtsame Abfrage am ehesten eine
* Akte verrät, die man nicht sehen darf. * Akte verrät, die man nicht sehen darf.
*/ */
export default async function DashboardPage() { export default async function DashboardPage() {
@@ -206,13 +206,13 @@ function Block({
/** /**
* Eine Zeile der Sprungliste. Sie kann eine Akte, eine Person oder ein * Eine Zeile der Sprungliste. Sie kann eine Akte, eine Person oder ein
* Dokument sein — deshalb steht die Art nicht dabei, sondern nur das, woran * Dokument sein - deshalb steht die Art nicht dabei, sondern nur das, woran
* man sie erkennt: Aktenzeichen, Name oder Dokumentnummer. * man sie erkennt: Aktenzeichen, Name oder Dokumentnummer.
* *
* Eine Akte und ein Dokument tragen eine **Kennung**, eine Person nicht: ihr * Eine Akte und ein Dokument tragen eine **Kennung**, eine Person nicht: ihr
* Name ist der Titel. Stünde er in der Kennungsspalte, stünde er in Mono, wäre * Name ist der Titel. Stünde er in der Kennungsspalte, stünde er in Mono, wäre
* abgeschnitten, und der Dienstgrad rutschte dorthin, wo sonst der Titel * abgeschnitten, und der Dienstgrad rutschte dorthin, wo sonst der Titel
* steht — untereinander ergäbe das keine Spalte mehr. * steht - untereinander ergäbe das keine Spalte mehr.
* *
* Ohne Vorabruf, anders als die übrigen Listen: die Seiten dahinter merken * Ohne Vorabruf, anders als die übrigen Listen: die Seiten dahinter merken
* sich, dass sie geöffnet wurden. Der Vorabruf wird zwar serverseitig erkannt * sich, dass sie geöffnet wurden. Der Vorabruf wird zwar serverseitig erkannt
+1 -1
View File
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/** /**
* Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein. * Skelett statt Spinner: die Struktur steht sofort, die Daten fallen hinein.
* Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist — * Ohne diese Grenze wartet die Navigation stumm, bis der Server fertig ist -
* man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md. * man klickt und es passiert scheinbar nichts. Siehe docs/09-performance-und-ux.md.
*/ */
export default function Loading() { export default function Loading() {
+1 -1
View File
@@ -5,7 +5,7 @@ import { de } from '@/lib/i18n/de'
* *
* Bewusst **kein `notFound()`**: die Wurzel hat ein `loading.tsx`, und dessen * Bewusst **kein `notFound()`**: die Wurzel hat ein `loading.tsx`, und dessen
* Skelett bleibt in dieser Gruppe stehen, statt durch die Nicht-gefunden-Seite * Skelett bleibt in dieser Gruppe stehen, statt durch die Nicht-gefunden-Seite
* ersetzt zu werden — die Ansicht bliebe leer. Für ein Blatt, das gedruckt * ersetzt zu werden - die Ansicht bliebe leer. Für ein Blatt, das gedruckt
* wird, ist ein leeres Blatt der schlechteste Ausgang: es sieht aus wie ein * wird, ist ein leeres Blatt der schlechteste Ausgang: es sieht aus wie ein
* Fehler im Papier, nicht wie eine Auskunft. * Fehler im Papier, nicht wie eine Auskunft.
* *
+2 -2
View File
@@ -7,7 +7,7 @@ import { exportText } from '@/lib/i18n/export'
/** /**
* Die Leiste über einer Druckansicht: zurück zur Akte und drucken. * Die Leiste über einer Druckansicht: zurück zur Akte und drucken.
* *
* Die Druckansichten liegen ausserhalb des angemeldeten Rahmenwerks — der * Die Druckansichten liegen ausserhalb des angemeldeten Rahmenwerks - der
* PDF-Dienst ruft sie mit einem kurzlebigen Zeichen auf und hat keine Sitzung, * PDF-Dienst ruft sie mit einem kurzlebigen Zeichen auf und hat keine Sitzung,
* also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau * also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau
* deshalb gab es aber auch keinen Weg zurück: wer die Ansicht im Browser * deshalb gab es aber auch keinen Weg zurück: wer die Ansicht im Browser
@@ -15,7 +15,7 @@ import { exportText } from '@/lib/i18n/export'
* *
* Das Ziel wird aus dem Pfad abgeleitet und nicht hereingereicht: `/druck` * Das Ziel wird aus dem Pfad abgeleitet und nicht hereingereicht: `/druck`
* abschneiden ergibt die Akte, aus der man kommt. Das stimmt auch, wenn jemand * abschneiden ergibt die Akte, aus der man kommt. Das stimmt auch, wenn jemand
* die Adresse direkt aufruft — `router.back()` täte das nicht. * die Adresse direkt aufruft - `router.back()` täte das nicht.
* *
* `no-print` blendet die Leiste beim Drucken aus. Auf dem Papier hat ein * `no-print` blendet die Leiste beim Drucken aus. Auf dem Papier hat ein
* Zurück-Knopf nichts verloren. * Zurück-Knopf nichts verloren.
@@ -23,7 +23,7 @@ export async function generateMetadata({
/** /**
* Druckansicht eines einzelnen Dokuments. * Druckansicht eines einzelnen Dokuments.
* *
* Kein Verlauf, keine Kopfdatentabelle einer Akte — das Dokument selbst, so * Kein Verlauf, keine Kopfdatentabelle einer Akte - das Dokument selbst, so
* wie seine Vorlage es vorsieht. Ein Aktenblatt kommt als Aktenblatt heraus, * wie seine Vorlage es vorsieht. Ein Aktenblatt kommt als Aktenblatt heraus,
* ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md. * ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md.
*/ */
@@ -169,7 +169,7 @@ export default async function PrintDocumentPage({
</p> </p>
) : ( ) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}> <p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')} {String(section.value ?? '-')}
</p> </p>
)} )}
</div> </div>
@@ -99,7 +99,7 @@ export default async function PrintCasePage({
/> />
<Zeile <Zeile
label={de.case.dueAt} label={de.case.dueAt}
value={akte.dueAt ? akte.dueAt.toLocaleDateString('de-DE') : '—'} value={akte.dueAt ? akte.dueAt.toLocaleDateString('de-DE') : '-'}
/> />
<Zeile label={de.export.retrievedBy} value={displayName} /> <Zeile label={de.export.retrievedBy} value={displayName} />
<Zeile <Zeile
@@ -164,7 +164,7 @@ export default async function PrintCasePage({
<td>{anhang.originalName}</td> <td>{anhang.originalName}</td>
<td>{anhang.sizeBytes}</td> <td>{anhang.sizeBytes}</td>
{/* Der Hash macht nachprüfbar, dass die Datei zum Ausdruck {/* Der Hash macht nachprüfbar, dass die Datei zum Ausdruck
gehört — deshalb steht er im Verzeichnis. */} gehört - deshalb steht er im Verzeichnis. */}
<td style={{ fontFamily: 'monospace', fontSize: '7pt', wordBreak: 'break-all' }}> <td style={{ fontFamily: 'monospace', fontSize: '7pt', wordBreak: 'break-all' }}>
{anhang.sha256} {anhang.sha256}
</td> </td>
@@ -188,7 +188,7 @@ function Zeile({ label, value }: { label: string; value: string }) {
/** /**
* Die Abschnitte im Druck. Ein gesperrtes Feld wird als Balken ausgegeben, mit * Die Abschnitte im Druck. Ein gesperrtes Feld wird als Balken ausgegeben, mit
* dem Vermerk, ab welcher Stufe es lesbar wäre — sonst merkt der Empfänger * dem Vermerk, ab welcher Stufe es lesbar wäre - sonst merkt der Empfänger
* nicht, dass er eine gekürzte Fassung in der Hand hält. * nicht, dass er eine gekürzte Fassung in der Hand hält.
*/ */
function PrintSections({ function PrintSections({
@@ -256,7 +256,7 @@ function PrintSections({
</p> </p>
) : ( ) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}> <p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')} {String(section.value ?? '-')}
</p> </p>
)} )}
</div> </div>
+2 -2
View File
@@ -10,9 +10,9 @@ export const dynamic = 'force-dynamic'
* kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung * kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung
* ergäbe ein leeres PDF. Die Prüfung steht in den Seiten selbst. * ergäbe ein leeres PDF. Die Prüfung steht in den Seiten selbst.
* *
* Was er tut, ist zweierlei: er legt den weissen Grund unter das Blatt — sonst * Was er tut, ist zweierlei: er legt den weissen Grund unter das Blatt - sonst
* schwebt ein Ausdruck im dunklen Thema der Anwendung, und man hält die * schwebt ein Ausdruck im dunklen Thema der Anwendung, und man hält die
* Ansicht für kaputt — und er gibt einen Weg zurück. Beides verschwindet beim * Ansicht für kaputt - und er gibt einen Weg zurück. Beides verschwindet beim
* Drucken. * Drucken.
*/ */
export default function PrintLayout({ children }: { children: ReactNode }) { export default function PrintLayout({ children }: { children: ReactNode }) {
+1 -1
View File
@@ -18,7 +18,7 @@ export const dynamic = 'force-dynamic'
* GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten * GET /api/admin/export?kind=bundle ZIP mit den fertigen Exporten
* *
* Beide nur mit Recht `admin`, beide protokolliert. Der JSON-Abzug ist die * Beide nur mit Recht `admin`, beide protokolliert. Der JSON-Abzug ist die
* einzige Stelle im System, an der die Klassifizierung nicht greift — deshalb * einzige Stelle im System, an der die Klassifizierung nicht greift - deshalb
* liegt er hinter einem eigenen Endpunkt und nicht hinter einem Knopf an einer * liegt er hinter einem eigenen Endpunkt und nicht hinter einem Knopf an einer
* Akte. * Akte.
*/ */
+1 -1
View File
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
* Herunterladen eines fertigen Exports. * Herunterladen eines fertigen Exports.
* *
* Nur der Auftraggeber selbst. Ein Export ist auf **seine** Stufe gekürzt und * Nur der Auftraggeber selbst. Ein Export ist auf **seine** Stufe gekürzt und
* trägt **seinen** Namen im Wasserzeichen — ihn an jemand anderen auszuliefern * trägt **seinen** Namen im Wasserzeichen - ihn an jemand anderen auszuliefern
* hiesse, ein Dokument mit fremdem Namen zu verteilen. * hiesse, ein Dokument mit fremdem Namen zu verteilen.
*/ */
export async function GET( export async function GET(
+1 -1
View File
@@ -13,7 +13,7 @@ export const runtime = 'nodejs'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
/** /**
* Auslieferung. **Niemals als statische Datei** — jeder Abruf läuft durch die * Auslieferung. **Niemals als statische Datei** - jeder Abruf läuft durch die
* Sichtbarkeitsprüfung der zugehörigen Akte und wird ab Stufe 3 protokolliert. * Sichtbarkeitsprüfung der zugehörigen Akte und wird ab Stufe 3 protokolliert.
* Siehe docs/07-dateien-und-sicherheit.md. * Siehe docs/07-dateien-und-sicherheit.md.
* *
+1 -1
View File
@@ -16,7 +16,7 @@ export const dynamic = 'force-dynamic'
* Fortschrittsbalken, und 25 MB sind mehr, als eine Server Action annimmt. * Fortschrittsbalken, und 25 MB sind mehr, als eine Server Action annimmt.
* *
* Die Antwort kommt, sobald die Datei in der Quarantäne liegt. Die Prüfkette * Die Antwort kommt, sobald die Datei in der Quarantäne liegt. Die Prüfkette
* läuft danach weiter und meldet sich über `/api/events` — niemand wartet mit * läuft danach weiter und meldet sich über `/api/events` - niemand wartet mit
* laufendem Balken auf einen Virenscanner. Siehe docs/09. * laufendem Balken auf einen Virenscanner. Siehe docs/09.
*/ */
export async function POST(request: Request): Promise<Response> { export async function POST(request: Request): Promise<Response> {
+2 -2
View File
@@ -18,7 +18,7 @@ const RECENT_IN_PALETTE = 6
* Schnellsuche für die Kommandopalette. * Schnellsuche für die Kommandopalette.
* *
* Bewusst schmal: Aktenzeichen, Titel und Namen, kein Volltext über Inhalte. * Bewusst schmal: Aktenzeichen, Titel und Namen, kein Volltext über Inhalte.
* Wer die Palette öffnet, will springen, nicht recherchieren — dafür gibt es * Wer die Palette öffnet, will springen, nicht recherchieren - dafür gibt es
* `/suche`. * `/suche`.
* *
* Der Trigram-Index auf `caseNumber` und `displayName` fängt Tippfehler ab, * Der Trigram-Index auf `caseNumber` und `displayName` fängt Tippfehler ab,
@@ -31,7 +31,7 @@ export async function GET(request: Request): Promise<Response> {
const params = new URL(request.url).searchParams const params = new URL(request.url).searchParams
// Leeres Feld: die eigene Sprungliste. Wer die Palette öffnet, will meistens // Leeres Feld: die eigene Sprungliste. Wer die Palette öffnet, will meistens
// dorthin zurück, wo er eben war — und nicht erst tippen. // dorthin zurück, wo er eben war - und nicht erst tippen.
if (params.get('recent') === '1') { if (params.get('recent') === '1') {
const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE) const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE)
return NextResponse.json({ return NextResponse.json({
+3 -3
View File
@@ -30,7 +30,7 @@ const HEARTBEAT_MS = 25_000
* *
* Drei Prüfungen, alle nötig: * Drei Prüfungen, alle nötig:
* 1. **Beim Verbinden**: welche der angefragten Themen darf dieser Nutzer * 1. **Beim Verbinden**: welche der angefragten Themen darf dieser Nutzer
* überhaupt abonnieren. Wer ein fremdes Thema nennt, bekommt es nicht — * überhaupt abonnieren. Wer ein fremdes Thema nennt, bekommt es nicht -
* und erfährt auch nicht, ob es existiert. * und erfährt auch nicht, ob es existiert.
* 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen. * 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen.
* Eine Umklassifizierung mitten in einer offenen Verbindung wirkt dadurch * Eine Umklassifizierung mitten in einer offenen Verbindung wirkt dadurch
@@ -38,7 +38,7 @@ const HEARTBEAT_MS = 25_000
* 3. **Laufend**: Sitzung und Rechte werden neu aufgelöst, nicht einmal beim * 3. **Laufend**: Sitzung und Rechte werden neu aufgelöst, nicht einmal beim
* Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im * Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im
* System, an dem ein Entzug nicht ankommt: der Worker löscht die Sitzung, * System, an dem ein Entzug nicht ankommt: der Worker löscht die Sitzung,
* jeder Seitenaufruf ist abgemeldet — und der Strom läuft weiter, weil * jeder Seitenaufruf ist abgemeldet - und der Strom läuft weiter, weil
* niemand mehr fragt. Siehe docs/03-auth-und-rechte.md. * niemand mehr fragt. Siehe docs/03-auth-und-rechte.md.
*/ */
export async function GET(request: Request): Promise<Response> { export async function GET(request: Request): Promise<Response> {
@@ -161,7 +161,7 @@ export async function GET(request: Request): Promise<Response> {
return return
} }
// Rechte geändert: die erlaubten Themen gelten nicht mehr. Sie neu // Rechte geändert: die erlaubten Themen gelten nicht mehr. Sie neu
// zu bestimmen ist wichtiger als die eine Zustellung — ein Ereignis // zu bestimmen ist wichtiger als die eine Zustellung - ein Ereignis
// trägt bei Verlaufseinträgen nur eine Kennung, das Thema ist dort // trägt bei Verlaufseinträgen nur eine Kennung, das Thema ist dort
// die **einzige** Schranke. // die **einzige** Schranke.
if (abdruck(frisch) !== abdruck(rechte)) { if (abdruck(frisch) !== abdruck(rechte)) {
+1 -1
View File
@@ -17,7 +17,7 @@ const BEWERTUNGEN = new Set(['good', 'needs-improvement', 'poor'])
* Zeichenketten aus dem Browser speichert, ist ein Ablagefach für Fremde. * Zeichenketten aus dem Browser speichert, ist ein Ablagefach für Fremde.
* *
* Der Nutzer wird **nicht** mitgeschrieben. Wer wann welche Akte geöffnet hat, * Der Nutzer wird **nicht** mitgeschrieben. Wer wann welche Akte geöffnet hat,
* steht im Zugriffsprotokoll — dort gehört es hin, dort ist es geregelt, und * steht im Zugriffsprotokoll - dort gehört es hin, dort ist es geregelt, und
* eine zweite Spur daneben wäre eine Überwachungsfunktion ohne Auftrag. * eine zweite Spur daneben wäre eine Überwachungsfunktion ohne Auftrag.
*/ */
export async function POST(request: Request): Promise<Response> { export async function POST(request: Request): Promise<Response> {
+7 -7
View File
@@ -56,7 +56,7 @@
/* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund. /* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund.
Eine Akte ist ein Blatt und hat scharfe Kanten. Was man druecken oder Eine Akte ist ein Blatt und hat scharfe Kanten. Was man druecken oder
ausfuellen kann, ist rund — daran erkennt man es, bevor man es beruehrt. */ ausfuellen kann, ist rund - daran erkennt man es, bevor man es beruehrt. */
--radius-none: 0px; --radius-none: 0px;
--radius-sm: 2px; --radius-sm: 2px;
--radius-DEFAULT: 2px; --radius-DEFAULT: 2px;
@@ -87,7 +87,7 @@
* Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe * Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe
* haben, sonst lernt man sie zweimal. Siehe docs/10-design-system.md. * haben, sonst lernt man sie zweimal. Siehe docs/10-design-system.md.
* *
* Kein warmes Cremeweiss, sondern ein leicht kühles Papierweiss — dieselbe * Kein warmes Cremeweiss, sondern ein leicht kühles Papierweiss - dieselbe
* Anmutung wie das dunkle Thema, nur andersherum. Die Kontraste sind auf * Anmutung wie das dunkle Thema, nur andersherum. Die Kontraste sind auf
* mindestens 4,5:1 für Fliesstext ausgelegt. */ * mindestens 4,5:1 für Fliesstext ausgelegt. */
[data-theme='light'] { [data-theme='light'] {
@@ -226,7 +226,7 @@
**Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.** **Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.**
Beim Druecken zieht sich der Knopf leicht zusammen und bekommt einen Beim Druecken zieht sich der Knopf leicht zusammen und bekommt einen
inneren Ring — er wirkt kurz dicker, ohne dass sich die Rahmenbreite inneren Ring - er wirkt kurz dicker, ohne dass sich die Rahmenbreite
wirklich aendert, sonst wuerde das Umfeld springen. Die Rueckmeldung wirklich aendert, sonst wuerde das Umfeld springen. Die Rueckmeldung
setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick
bliebe sonst unsichtbar. Siehe docs/10-design-system.md. bliebe sonst unsichtbar. Siehe docs/10-design-system.md.
@@ -318,7 +318,7 @@
Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein
Titel, etwas Beiwerk, eine Stempelmarke. Bisher bestimmte **jede Zeile Titel, etwas Beiwerk, eine Stempelmarke. Bisher bestimmte **jede Zeile
ihre Spaltenbreiten selbst** — die Kennung war so breit wie ihr Text, und ihre Spaltenbreiten selbst** - die Kennung war so breit wie ihr Text, und
damit fing der Titel in jeder Zeile woanders an. Bei „ETHIK" und „WS" damit fing der Titel in jeder Zeile woanders an. Bei „ETHIK" und „WS"
untereinander sieht man das sofort. untereinander sieht man das sofort.
@@ -350,7 +350,7 @@
} }
/* Wie die Kennung, aber fuer eine Bezeichnung aus einer festen Liste: /* Wie die Kennung, aber fuer eine Bezeichnung aus einer festen Liste:
die Rolle eines Beteiligten, die Art eines Auftrags. Kein Mono — es ist die Rolle eines Beteiligten, die Art eines Auftrags. Kein Mono - es ist
Sprache, keine Nummer. */ Sprache, keine Nummer. */
.record-label { .record-label {
flex: none; flex: none;
@@ -364,7 +364,7 @@
} }
/* Der Dienstgrad vor einem Namen. In derselben Groesse wie der Name, denn /* Der Dienstgrad vor einem Namen. In derselben Groesse wie der Name, denn
er gehoert zu ihm — aber in einer eigenen Spalte, sonst faengt jeder Name er gehoert zu ihm - aber in einer eigenen Spalte, sonst faengt jeder Name
woanders an. */ woanders an. */
.record-rank { .record-rank {
flex: none; flex: none;
@@ -827,7 +827,7 @@
/* Der Grund unter einer Druckansicht. /* Der Grund unter einer Druckansicht.
Ohne ihn liegt ein weisses Blatt auf dem dunklen Thema der Anwendung, und Ohne ihn liegt ein weisses Blatt auf dem dunklen Thema der Anwendung, und
die Ansicht sieht kaputt aus, obwohl sie stimmt. Beim Drucken faellt er die Ansicht sieht kaputt aus, obwohl sie stimmt. Beim Drucken faellt er
weg — Papier bringt seinen eigenen Grund mit. */ weg - Papier bringt seinen eigenen Grund mit. */
.print-page { .print-page {
min-height: 100dvh; min-height: 100dvh;
background: #f2f2f0; background: #f2f2f0;
+2 -2
View File
@@ -21,7 +21,7 @@ export const viewport: Viewport = {
* *
* Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine * Ohne das blitzt beim Laden kurz das falsche Thema auf. Es ist die eine
* Stelle mit `dangerouslySetInnerHTML`, und der Inhalt ist eine Konstante aus * Stelle mit `dangerouslySetInnerHTML`, und der Inhalt ist eine Konstante aus
* diesem Modul — kein Nutzertext. Das Verbot aus docs/07 gilt Nutzerinhalten. * diesem Modul - kein Nutzertext. Das Verbot aus docs/07 gilt Nutzerinhalten.
*/ */
const THEME_SCRIPT = const THEME_SCRIPT =
`try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` + `try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
@@ -38,7 +38,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
{/* {/*
`suppressHydrationWarning` wegen des Nonce, nicht wegen des Inhalts. `suppressHydrationWarning` wegen des Nonce, nicht wegen des Inhalts.
Der Browser **leert das Nonce-Attribut**, sobald er das Element Der Browser **leert das Nonce-Attribut**, sobald er das Element
geparst hat — so schreibt es die CSP-Spezifikation vor, damit kein geparst hat - so schreibt es die CSP-Spezifikation vor, damit kein
Skript den Wert über `getAttribute` auslesen und sich selbst Skript den Wert über `getAttribute` auslesen und sich selbst
nachladen kann. React vergleicht beim Hydrieren aber genau dieses nachladen kann. React vergleicht beim Hydrieren aber genau dieses
Attribut, liest clientseitig den leeren Wert und meldet einen Attribut, liest clientseitig den leeren Wert und meldet einen
+2 -2
View File
@@ -12,7 +12,7 @@ export { SIGN_IN_ERRORS }
/** /**
* Auth.js leitet die Callback-URL **nicht** aus dem Host-Header ab, sondern aus * Auth.js leitet die Callback-URL **nicht** aus dem Host-Header ab, sondern aus
* `AUTH_URL`. Fehlt die Variable, bleibt die `redirect_uri` fest bei * `AUTH_URL`. Fehlt die Variable, bleibt die `redirect_uri` fest bei
* `http://localhost:3000` — hinter dem Cloudflare Tunnel wäre die Anmeldung * `http://localhost:3000` - hinter dem Cloudflare Tunnel wäre die Anmeldung
* damit kaputt, und zwar mit einer Fehlermeldung von Discord, die nicht auf die * damit kaputt, und zwar mit einer Fehlermeldung von Discord, die nicht auf die
* Ursache zeigt. * Ursache zeigt.
* *
@@ -38,7 +38,7 @@ type DiscordProfile = {
* benutzt: die Rollen holt der Bot mit seinem eigenen Token. * benutzt: die Rollen holt der Bot mit seinem eigenen Token.
* *
* Die Sitzung selbst steht in der Datenbank. Im Cookie liegt nur ein * Die Sitzung selbst steht in der Datenbank. Im Cookie liegt nur ein
* signiertes Wertepaar aus `userId` und `sessionId` — ohne die Zeile in * signiertes Wertepaar aus `userId` und `sessionId` - ohne die Zeile in
* `Session` ist es wertlos. Nur so lässt sich eine Sitzung zurückziehen, * `Session` ist es wertlos. Nur so lässt sich eine Sitzung zurückziehen,
* wenn jemand seine Rolle verliert. Siehe docs/03-auth-und-rechte.md. * wenn jemand seine Rolle verliert. Siehe docs/03-auth-und-rechte.md.
*/ */
+2 -2
View File
@@ -6,13 +6,13 @@ import { commonText } from '@/lib/i18n/common'
/** /**
* Ein aufklappbares Formular zum Hinzufügen. * Ein aufklappbares Formular zum Hinzufügen.
* *
* Beteiligte, Verknüpfungen, Dokumente, Zuordnungen — überall derselbe Ablauf: * Beteiligte, Verknüpfungen, Dokumente, Zuordnungen - überall derselbe Ablauf:
* ein Knopf, ein kleines Formular, eine Server Action, eine Fehlermeldung an * ein Knopf, ein kleines Formular, eine Server Action, eine Fehlermeldung an
* Ort und Stelle. Das viermal zu wiederholen kostete nicht nur Zeilen, sondern * Ort und Stelle. Das viermal zu wiederholen kostete nicht nur Zeilen, sondern
* auch Bündelgrösse auf der Fallakte, die ohnehin die dichteste Seite ist. * auch Bündelgrösse auf der Fallakte, die ohnehin die dichteste Seite ist.
* *
* Der Rückruf bekommt `schliessen`, damit das Formular nur nach einem Erfolg * Der Rückruf bekommt `schliessen`, damit das Formular nur nach einem Erfolg
* zugeht — ein Fehler soll die Eingabe nicht wegräumen. * zugeht - ein Fehler soll die Eingabe nicht wegräumen.
*/ */
export function AddPanel({ export function AddPanel({
label, label,
+1 -1
View File
@@ -6,7 +6,7 @@
* im Stack für ein Bild von 64 Pixeln. Dafür steht `cdn.discordapp.com` in der * im Stack für ein Bild von 64 Pixeln. Dafür steht `cdn.discordapp.com` in der
* Inhaltsrichtlinie, siehe middleware.ts. * Inhaltsrichtlinie, siehe middleware.ts.
* *
* Ohne Bild — und das ist der Normalfall bei Konten ohne eigenes Profilbild — * Ohne Bild - und das ist der Normalfall bei Konten ohne eigenes Profilbild -
* stehen die Anfangsbuchstaben da. Kein Platzhaltergesicht: ein leerer Kreis * stehen die Anfangsbuchstaben da. Kein Platzhaltergesicht: ein leerer Kreis
* mit Initialen ist ehrlicher und lädt nichts nach. * mit Initialen ist ehrlicher und lädt nichts nach.
*/ */
+1 -1
View File
@@ -7,7 +7,7 @@ import { Pending } from '@/components/Pending'
/** /**
* Die Bereiche der Anwendung in der Kopfzeile. * Die Bereiche der Anwendung in der Kopfzeile.
* *
* Sie waren blosse Textlinks — von einem Fliesstextlink nicht zu unterscheiden * Sie waren blosse Textlinks - von einem Fliesstextlink nicht zu unterscheiden
* und ohne Hinweis darauf, wo man gerade ist. Jetzt tragen sie dieselbe * und ohne Hinweis darauf, wo man gerade ist. Jetzt tragen sie dieselbe
* Knopfform wie alles Bedienbare, und der Bereich, in dem man steht, ist * Knopfform wie alles Bedienbare, und der Bereich, in dem man steht, ist
* gefüllt. * gefüllt.
+2 -2
View File
@@ -6,12 +6,12 @@ import { useLinkStatus } from 'next/link'
* Der Ladepunkt in einem Link, solange der Wechsel läuft. * Der Ladepunkt in einem Link, solange der Wechsel läuft.
* *
* Ein Wechsel kann einen Moment dauern, und ohne Rückmeldung sieht es aus, als * Ein Wechsel kann einen Moment dauern, und ohne Rückmeldung sieht es aus, als
* sei der Klick nicht angekommen — man klickt ein zweites Mal. Der Punkt sitzt * sei der Klick nicht angekommen - man klickt ein zweites Mal. Der Punkt sitzt
* **im geklickten Link**, damit sichtbar bleibt, wohin es geht. * **im geklickten Link**, damit sichtbar bleibt, wohin es geht.
* *
* Das ist kein Blockade-Spinner im Sinn von Zusicherung 5 der README: er ersetzt * Das ist kein Blockade-Spinner im Sinn von Zusicherung 5 der README: er ersetzt
* keinen Inhalt und hält nichts auf. Die Zielseite bringt ihr Skelett mit, das * keinen Inhalt und hält nichts auf. Die Zielseite bringt ihr Skelett mit, das
* ist die eigentliche Antwort auf das Warten — hier geht es nur darum, den * ist die eigentliche Antwort auf das Warten - hier geht es nur darum, den
* Klick zu quittieren. * Klick zu quittieren.
* *
* `useLinkStatus` funktioniert nur als Kind eines `next/link`, deshalb die * `useLinkStatus` funktioniert nur als Kind eines `next/link`, deshalb die
+2 -2
View File
@@ -27,7 +27,7 @@ export function PieChart({
}: { }: {
slices: Slice[] slices: Slice[]
emptyLabel: string emptyLabel: string
/** „12 Fälle" — steht in der Mitte und sagt, worauf sich die Anteile beziehen. */ /** „12 Fälle" - steht in der Mitte und sagt, worauf sich die Anteile beziehen. */
totalLabel: (total: number) => string totalLabel: (total: number) => string
}) { }) {
const gesamt = slices.reduce((summe, stueck) => summe + stueck.value, 0) const gesamt = slices.reduce((summe, stueck) => summe + stueck.value, 0)
@@ -101,7 +101,7 @@ export function PieChart({
/** /**
* Abstufungen von `--steel`, gleichmässig verteilt. Bei einem einzigen * Abstufungen von `--steel`, gleichmässig verteilt. Bei einem einzigen
* Segment die volle Farbe, sonst von kräftig nach blass — so bleibt die * Segment die volle Farbe, sonst von kräftig nach blass - so bleibt die
* Reihenfolge der Liste auch im Kreis ablesbar. * Reihenfolge der Liste auch im Kreis ablesbar.
*/ */
function farbeFuer(index: number, anzahl: number): string { function farbeFuer(index: number, anzahl: number): string {
+1 -1
View File
@@ -19,7 +19,7 @@ import { ThemeToggle } from '@/components/ThemeToggle'
* das Abmelden. In der Kopfzeile standen diese Dinge vorher zwischen den * das Abmelden. In der Kopfzeile standen diese Dinge vorher zwischen den
* Aktenbereichen und nahmen dort Platz weg, den die Arbeit braucht. * Aktenbereichen und nahmen dort Platz weg, den die Arbeit braucht.
* *
* Das Abmelden kommt als Server Action von aussen herein — dieses Fenster darf * Das Abmelden kommt als Server Action von aussen herein - dieses Fenster darf
* die Sitzung nicht selbst beenden können. * die Sitzung nicht selbst beenden können.
*/ */
export function ProfileMenu({ export function ProfileMenu({
+1 -1
View File
@@ -3,7 +3,7 @@ import { searchBarText } from '@/lib/i18n/shell'
/** /**
* Der Suchbalken in der Kopfzeile. * Der Suchbalken in der Kopfzeile.
* *
* Ein gewöhnliches Formular, das die Anfrage an `/suche` übergibt — kein * Ein gewöhnliches Formular, das die Anfrage an `/suche` übergibt - kein
* Skript, kein Zustand, keine Bytes im Bündel. Die eigentliche Suche mit * Skript, kein Zustand, keine Bytes im Bündel. Die eigentliche Suche mit
* Filtern, Facetten und Abbruch laufender Anfragen sitzt auf der Suchseite, * Filtern, Facetten und Abbruch laufender Anfragen sitzt auf der Suchseite,
* hier steht nur das Eingangstor. * hier steht nur das Eingangstor.
+2 -2
View File
@@ -17,7 +17,7 @@ export function stampColor(level: number): string {
* `ClearanceLevelConfig.colorToken` und `PersonnelStatusTag.colorToken` sind * `ClearanceLevelConfig.colorToken` und `PersonnelStatusTag.colorToken` sind
* Daten und im Adminbereich pflegbar. Sie landen als Name einer CSS-Variablen * Daten und im Adminbereich pflegbar. Sie landen als Name einer CSS-Variablen
* in einem `style`-Attribut, und was aus der Datenbank in ein Stilattribut * in einem `style`-Attribut, und was aus der Datenbank in ein Stilattribut
* geht, wird geprueft — nicht weil es heute jemand setzen koennte, sondern * geht, wird geprueft - nicht weil es heute jemand setzen koennte, sondern
* weil es morgen jemand koennen soll. Ein unbekannter Wert faellt auf die * weil es morgen jemand koennen soll. Ein unbekannter Wert faellt auf die
* neutrale Marke zurueck, statt eine Variable zu erfinden, die es nicht gibt. * neutrale Marke zurueck, statt eine Variable zu erfinden, die es nicht gibt.
*/ */
@@ -45,7 +45,7 @@ export function colorVariable(token: string | null | undefined): string {
* **Feste Breite.** Die Namen der Stufen sind unterschiedlich lang und im * **Feste Breite.** Die Namen der Stufen sind unterschiedlich lang und im
* Adminbereich aenderbar; unterschiedlich breite Marken untereinander lesen * Adminbereich aenderbar; unterschiedlich breite Marken untereinander lesen
* sich als Unordnung, nicht als Unterschied. Ein zu langer Name wird gekuerzt, * sich als Unordnung, nicht als Unterschied. Ein zu langer Name wird gekuerzt,
* die Stufenziffer davor bleibt immer stehen — sie ist die eigentliche * die Stufenziffer davor bleibt immer stehen - sie ist die eigentliche
* Auskunft. * Auskunft.
* *
* `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar, * `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar,
+1 -1
View File
@@ -5,7 +5,7 @@ import { colorVariable } from '@/components/StampMark'
* *
* Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus. * Mehrere zugleich: „Gekündigt" und „Verhaftet" schließen einander nicht aus.
* Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im * Die Namen und Farben kommen aus `PersonnelStatusTag` und sind im
* Adminbereich pflegbar — deshalb werden sie hereingereicht und nicht aus * Adminbereich pflegbar - deshalb werden sie hereingereicht und nicht aus
* `lib/i18n/de.ts` gelesen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch. * `lib/i18n/de.ts` gelesen. Siehe die Sprachkonvention: Code englisch, Oberflaeche deutsch.
*/ */
export function StatusTags({ export function StatusTags({
+1 -1
View File
@@ -8,7 +8,7 @@ import { Pending } from '@/components/Pending'
* Eine Reiterleiste. * Eine Reiterleiste.
* *
* Sie hebt hervor, wo man gerade ist. Vorher taten das weder die Reiter der * Sie hebt hervor, wo man gerade ist. Vorher taten das weder die Reiter der
* Fallakte noch die der Verwaltung — fünf gleich aussehende Links, und man * Fallakte noch die der Verwaltung - fünf gleich aussehende Links, und man
* musste an der Seite selbst ablesen, welcher davon offen war. * musste an der Seite selbst ablesen, welcher davon offen war.
* *
* Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse: * Der Vergleich läuft über den Pfad, nicht über die vollständige Adresse:
+1 -1
View File
@@ -12,7 +12,7 @@ export const THEME_KEY = 'afis:thema'
* *
* Drei Zustände, nicht zwei: „System" ist der Auslieferungszustand und folgt * Drei Zustände, nicht zwei: „System" ist der Auslieferungszustand und folgt
* `prefers-color-scheme`. Wer ausdrücklich wählt, bekommt seine Wahl auf * `prefers-color-scheme`. Wer ausdrücklich wählt, bekommt seine Wahl auf
* diesem Gerät gemerkt — nicht am Konto, denn es ist eine Eigenschaft des * diesem Gerät gemerkt - nicht am Konto, denn es ist eine Eigenschaft des
* Bildschirms, an dem jemand sitzt, nicht der Person. * Bildschirms, an dem jemand sitzt, nicht der Person.
* *
* Gesetzt wird das Attribut schon vor dem ersten Malen, siehe das Skript im * Gesetzt wird das Attribut schon vor dem ersten Malen, siehe das Skript im

Some files were not shown because too many files have changed in this diff Show More