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
@@ -6,7 +6,7 @@ import { visibleDocumentWhere } from '@/lib/access/personnelFilter'
/**
* Anhänge, die ein Nutzer sehen darf.
*
* Ein Anhang hat keine eigene Freigabestufe — er erbt die seines Besitzers.
* Ein Anhang hat keine eigene Freigabestufe - er erbt die seines Besitzers.
* Das ist die richtige Bauart: eine Datei an einem Verlaufseintrag der Stufe 4
* ist so geheim wie der Eintrag, und läge die Stufe am Anhang, müsste jemand
* beide gleichzeitig richtig setzen.
+2 -2
View File
@@ -16,13 +16,13 @@ import { WORKING_ROLES } from '@/lib/cases/terms'
* 4. Stufe reicht → Zugriff
* 5. sonst kein Zugriff
*
* Zu 3.: Die Beteiligtenliste beantwortet zwei Fragen — wer bearbeitet den
* Zu 3.: Die Beteiligtenliste beantwortet zwei Fragen - wer bearbeitet den
* Fall, und um wen geht es darin. Nur die erste verschafft Zugang. Ein
* Betroffener, ein Zeuge oder ein Täter mit Discord-Konto läse sonst die
* Ermittlung gegen sich selbst mit. Siehe lib/cases/terms.ts.
*
* Der Ausschluss steht auch über dem Admin. Das Codebeispiel im Dokument gibt
* für Admins `{}` zurück und übergeht ihn damit — das widerspricht der
* für Admins `{}` zurück und übergeht ihn damit - das widerspricht der
* Reihenfolge eine Zeile darüber. Hier gilt die Reihenfolge: der Ausschluss
* deckt gerade den Fall ab, dass gegen einen hochrangigen Mitarbeiter
* ermittelt wird, und der Administrator ist einer.
+2 -2
View File
@@ -27,7 +27,7 @@ function requirePermission(p: EffectivePermissions, permission: Permission): Wri
/**
* Darf dieser Nutzer eine Akte mit dieser Klassifizierung anlegen?
*
* Niemand legt eine Akte über der eigenen Stufe an — auch kein Admin, denn er
* Niemand legt eine Akte über der eigenen Stufe an - auch kein Admin, denn er
* hätte sonst eine Akte erzeugt, deren Sichtbarkeit er nicht überblickt.
* Der Admin hat ohnehin Stufe 5.
*/
@@ -78,7 +78,7 @@ export function canEditCase(
*
* Die neue Stufe darf die eigene nicht übersteigen. Eine Herabstufung ist der
* heikelste Vorgang im System: sie macht Inhalte für Leute sichtbar, die sie
* vorher nicht sehen durften. Erlaubt ist sie trotzdem — sie wird nur sichtbar
* vorher nicht sehen durften. Erlaubt ist sie trotzdem - sie wird nur sichtbar
* protokolliert, im Verlauf und im Zugriffsprotokoll.
*/
export function canReclassify(
+1 -1
View File
@@ -8,7 +8,7 @@ import { visibleDepartmentWhere } from '@/lib/access/departmentFilter'
* Chat: der formlose Teil, der **an einer Akte hängt**.
*
* Es gibt keine freien Räume mehr. Ein Chat gehört zu einer Fallakte oder zu
* einer Personalakte und erbt deren Sichtbarkeit — wer die Akte nicht lesen
* einer Personalakte und erbt deren Sichtbarkeit - wer die Akte nicht lesen
* darf, sieht auch nicht, dass darüber geredet wird. Damit gibt es keinen Ort
* im System, an dem über einen Vorgang gesprochen werden kann, ohne dass das
* Gespräch beim Vorgang steht. Siehe docs/13-entscheidungen.md.
+1 -1
View File
@@ -13,7 +13,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
* allgemein bekannt sein soll. Und sie gilt dann für alles, was **an** der
* Abteilung hängt: ihre Vermerke und ihren Chat.
*
* Was in ihr aktenkundig ist, filtert dagegen weiterhin jede Liste für sich —
* Was in ihr aktenkundig ist, filtert dagegen weiterhin jede Liste für sich -
* die Mitglieder über `visiblePersonnelWhere`, die Fälle über
* `visibleCaseWhere`. Eine sichtbare Abteilung ist kein Zugang zu ihrem Inhalt.
*/
+1 -1
View File
@@ -36,7 +36,7 @@ export function canWritePersonnel(
/**
* Darf dieser Nutzer ein Dokument mit dieser Klassifizierung anlegen?
*
* Der Besitzer ist vorher schon durch die Sichtbarkeitsprüfung gelaufen — was
* Der Besitzer ist vorher schon durch die Sichtbarkeitsprüfung gelaufen - was
* man nicht sieht, kann man nicht bestücken.
*/
export function canCreateDocument(
+2 -2
View File
@@ -44,13 +44,13 @@ export function visiblePersonnelWhere(p: EffectivePermissions): Prisma.Personnel
* Zwei Bedingungen, beide müssen zutreffen:
*
* 1. **Der Besitzer muss lesbar sein.** Ein Vertrag an einer Personalakte der
* Stufe 4 bleibt verborgen, auch wenn der Vertrag selbst Stufe 1 trägt —
* Stufe 4 bleibt verborgen, auch wenn der Vertrag selbst Stufe 1 trägt -
* sonst verriete die Dokumentenliste die Existenz der Person.
* 2. **Das Dokument selbst muss lesbar sein.** Innerhalb einer sichtbaren
* Akte kann ein einzelnes Dokument höher klassifiziert sein.
*
* Wer an der besitzenden Fallakte beteiligt ist, liest ihre Dokumente
* unabhängig von seiner Stufe — das ist derselbe ausnahmsweise Zugang wie beim
* unabhängig von seiner Stufe - das ist derselbe ausnahmsweise Zugang wie beim
* Verlauf, siehe docs/03-auth-und-rechte.md. Für Dokumente an einer
* Personalakte und für Dokumente ohne Besitzer gibt es diesen Weg nicht.
*/
+3 -3
View File
@@ -37,7 +37,7 @@ export type AuditRow = {
action: string
targetType: string
targetId: string
/** Aktenzeichen, Name oder Dokumentnummer — was man wiedererkennt. */
/** Aktenzeichen, Name oder Dokumentnummer - was man wiedererkennt. */
targetLabel: string | null
targetHref: string | null
clearanceAtTime: number
@@ -109,7 +109,7 @@ async function namenVon(ids: string[]): Promise<Map<string, string>> {
* Aus Kennungen werden Aktenzeichen und Namen.
*
* Ohne das steht im Protokoll eine Spalte UUIDs, und niemand liest es. Der
* Nachschlag läuft **ohne** Sichtbarkeitsfilter — das Protokoll ist die
* Nachschlag läuft **ohne** Sichtbarkeitsfilter - das Protokoll ist die
* Ausnahme, die es geben muss, damit die Leitung prüfen kann, ob die Regeln
* eingehalten wurden. Genau deshalb kommt hier auch nur die Leitung hin.
*/
@@ -167,7 +167,7 @@ async function zieleVon(
return karte
}
/** Die Akteure, die überhaupt im Protokoll vorkommen — für die Auswahlliste. */
/** Die Akteure, die überhaupt im Protokoll vorkommen - für die Auswahlliste. */
export async function auditActors(): Promise<{ id: string; name: string }[]> {
const gruppen = await db.auditLog.groupBy({ by: ['actorId'], _count: { actorId: true } })
const namen = await namenVon(gruppen.map((g) => g.actorId))
+2 -2
View File
@@ -23,8 +23,8 @@ export type CurrentUser = {
* 2. Die Sitzungszeile existiert noch und ist nicht abgelaufen
* 3. Der Nutzer trägt weiterhin eine Rolle, die zur Anmeldung berechtigt
*
* Schritt 3 ist doppelt gemoppelt — der Worker beendet Sitzungen beim
* Rollenverlust bereits von sich aus — aber genau das ist der Sinn: fällt der
* Schritt 3 ist doppelt gemoppelt - der Worker beendet Sitzungen beim
* Rollenverlust bereits von sich aus - aber genau das ist der Sinn: fällt der
* Worker aus, verliert trotzdem niemand unbemerkt seinen Ausschluss.
*/
export async function getCurrentUser(): Promise<CurrentUser | null> {
+1 -1
View File
@@ -232,7 +232,7 @@ export async function restoreEntry(
/**
* Änderungsübersicht einer Akte: alle nachträglichen Änderungen am Verlauf,
* chronologisch. Sichtbar für alle, die die Akte lesen dürfen — das ist der
* chronologisch. Sichtbar für alle, die die Akte lesen dürfen - das ist der
* ganze Sinn der Sache.
*/
export async function loadChangeHistory(p: EffectivePermissions, caseId: string) {
+1 -1
View File
@@ -14,7 +14,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
* anderes, je nachdem, von welcher Seite man liest. `RELATED` ist symmetrisch.
*
* Beide Seiten laufen durch die Sichtbarkeitsprüfung. Eine Akte, die der Leser
* nicht sehen darf, taucht auch als Verknüpfung nicht auf — sonst verriete die
* nicht sehen darf, taucht auch als Verknüpfung nicht auf - sonst verriete die
* Gegenseite ihre Existenz. Das gilt in **beide** Richtungen, deshalb wird der
* Filter zweimal angewandt.
*/
+3 -3
View File
@@ -30,7 +30,7 @@ export async function listParticipants(p: EffectivePermissions, caseId: string)
addedById: true,
user: { select: { id: true, displayName: true } },
// Die Personalakte kommt nur mit, wenn der Leser sie sehen darf. Sonst
// steht der Name da, aber der Verweis führt ins Leere — das ist richtig
// steht der Name da, aber der Verweis führt ins Leere - das ist richtig
// so: der Beteiligte ist Teil der Akte, seine Personalakte nicht.
personnelFile: { select: { id: true, displayName: true } },
},
@@ -92,7 +92,7 @@ export async function addParticipant(
}
if (personnelFileId) {
// Eine Personalakte, die der Eintragende nicht sehen darf, kann er auch
// nicht als Beteiligte setzen — sonst verrät ihm die Fehlermeldung ihre
// nicht als Beteiligte setzen - sonst verrät ihm die Fehlermeldung ihre
// Existenz.
const akte = await db.personnelFile.findFirst({
where: { AND: [visiblePersonnelWhere(p), { id: personnelFileId }] },
@@ -146,7 +146,7 @@ export async function addParticipant(
/**
* Entfernt einen Beteiligten. Anders als ein Verlaufseintrag ist das keine
* Aussage über den Sachverhalt, sondern eine Zuständigkeit — sie darf
* Aussage über den Sachverhalt, sondern eine Zuständigkeit - sie darf
* verschwinden. Das Protokoll behält sie.
*/
export async function removeParticipant(
+3 -3
View File
@@ -19,7 +19,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
export { AccessError } from '@/lib/errors'
/** Lädt eine Akte, sofern der Nutzer sie sehen darf. Sonst `null` — nie 403. */
/** Lädt eine Akte, sofern der Nutzer sie sehen darf. Sonst `null` - nie 403. */
export async function loadVisibleCase(p: EffectivePermissions, caseNumber: string) {
return db.case.findFirst({
where: { AND: [visibleCaseWhere(p), { caseNumber }] },
@@ -303,14 +303,14 @@ async function missingRequiredSections(caseId: string, templateId: string): Prom
/**
* Wiedervorlage setzen oder abräumen.
*
* Das Feld gab es von Anfang an — die Übersicht zeigt darauf ihre Fristen, die
* Das Feld gab es von Anfang an - die Übersicht zeigt darauf ihre Fristen, die
* Suche filtert darauf, der Ausdruck druckt es. Nur setzen konnte es niemand:
* es fehlte jede Stelle, an der man ein Datum einträgt. Deshalb stand unter
* „Fristen" immer „Nichts steht an". Siehe docs/13-entscheidungen.md.
*
* Kein Verlaufseintrag: eine Wiedervorlage ist eine Merkhilfe für die
* Bearbeitung und keine Aussage über den Fall. Protokolliert wird sie
* trotzdem — sie ist eine Änderung an der Akte, und dafür gilt harte Regel 3.
* trotzdem - sie ist eine Änderung an der Akte, und dafür gilt harte Regel 3.
*/
export async function setDueDate(
p: EffectivePermissions,
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* Feste Wertelisten der Akte — Rollen der Beteiligten und Arten der
* Feste Wertelisten der Akte - Rollen der Beteiligten und Arten der
* Verknüpfung.
*
* **Ohne Abhängigkeiten.** Diese Datei wird auch im Client geladen, und die
@@ -28,7 +28,7 @@ export function isParticipantRole(value: string): value is ParticipantRoleName {
}
/**
* Wer an der Akte **arbeitet** — und nur diese drei Rollen öffnen sie auch.
* Wer an der Akte **arbeitet** - und nur diese drei Rollen öffnen sie auch.
*
* Die Beteiligtenliste beantwortet zwei ganz verschiedene Fragen: *wer bearbeitet
* diesen Fall* und *um wen geht es darin*. Sie standen bisher gleichberechtigt
+1 -1
View File
@@ -10,7 +10,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
* Wer eine Akte beobachtet, sieht sie auf der Übersicht und bekommt ihre
* Meldungen, auch ohne beteiligt zu sein. **Zugriff verschafft das nicht**:
* beobachten kann man nur, was man ohnehin sehen darf, und fällt die Akte
* später über die eigene Stufe, verschwindet sie auch aus der Liste — die
* später über die eigene Stufe, verschwindet sie auch aus der Liste - die
* Abfrage läuft durch dieselbe Sichtbarkeitsprüfung wie die Fallliste.
*/
export async function listWatched(p: EffectivePermissions, take = 20) {
+1 -1
View File
@@ -6,7 +6,7 @@
* Client schickt Text, der Server bestimmt, wer gemeint ist.
*/
/** `@name` — Buchstaben, Ziffern, Punkt, Unterstrich, Bindestrich. */
/** `@name` - Buchstaben, Ziffern, Punkt, Unterstrich, Bindestrich. */
export const MENTION = /@([a-z0-9._-]{2,32})/gi
export function extractMentionNames(body: string): string[] {
+1 -1
View File
@@ -6,7 +6,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
* Vorschaukarten für Aktenzeichen im Chattext.
*
* **Serverseitig aufgelöst, mit der Stufe des Lesers.** Wer die Akte nicht
* sehen darf, bekommt eine neutrale Karte ohne Titel und ohne Status — nie den
* sehen darf, bekommt eine neutrale Karte ohne Titel und ohne Status - nie den
* Titel mitschicken und im Client ausblenden. Siehe docs/06.
*
* Ob eine Akte existiert, verrät die Karte damit auch nicht: eine unbekannte
+5 -5
View File
@@ -22,7 +22,7 @@ import type { Prisma } from '@afis/db'
/**
* Der Aktenchat.
*
* Er hängt immer an einer Akte — einer Fallakte oder einer Personalakte — und
* Er hängt immer an einer Akte - einer Fallakte oder einer Personalakte - und
* ist **ausdrücklich kein Aktenbestandteil**: er ist der formlose Teil, den
* die Abteilung bisher auf Discord hatte. Deshalb darf hier bearbeitet und
* zurückgezogen werden, ohne dass eine Version entsteht.
@@ -213,7 +213,7 @@ export async function sendMessage(
select: { id: true },
})
// Für den Worker. Hier steht nur, was hinausgehen darf — nie der Text der
// Für den Worker. Hier steht nur, was hinausgehen darf - nie der Text der
// Nachricht. Wer sie bekommt, entscheidet diese Anwendung, nicht er.
if (ort.caseId) {
const akte = await tx.case.findUniqueOrThrow({
@@ -250,7 +250,7 @@ export async function sendMessage(
return angelegt
}
/** Bearbeiten. Sichtbar über `editedAt`, ohne Versionierung — kein Aktenbestand. */
/** Bearbeiten. Sichtbar über `editedAt`, ohne Versionierung - kein Aktenbestand. */
export async function editMessage(
p: EffectivePermissions,
input: { id: string; body: string },
@@ -328,7 +328,7 @@ export async function listReactions(p: EffectivePermissions, messageIds: string[
* „Als Vermerk übernehmen".
*
* Die Brücke zwischen Discord-Gewohnheit und sauberem Aktenbestand: aus einer
* formlosen Nachricht wird ein Verlaufseintrag, der die harten Regeln erbt —
* formlosen Nachricht wird ein Verlaufseintrag, der die harten Regeln erbt -
* er ist ab da nicht mehr löschbar und jede Änderung erzeugt eine Version.
*
* Die Nachricht bleibt stehen und wird als übernommen markiert. Der Eintrag
@@ -436,7 +436,7 @@ function themaVon(nachricht: {
* Wann der Nutzer diesen Ort zuletzt gesehen hat.
*
* Der Marker wird beim Öffnen **nicht** sofort hochgesetzt, sondern erst, wenn
* der Nutzer die Ansicht verlässt oder ausdrücklich weiterliest — sonst wäre
* der Nutzer die Ansicht verlässt oder ausdrücklich weiterliest - sonst wäre
* die Trennlinie „Neu seit deinem letzten Besuch" beim Neuladen sofort weg.
*/
export async function loadReadMarker(
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Fester Satz an Reaktionen, keine freie Emoji-Auswahl.
*
* Ohne Abhängigkeiten, weil der Client sie lädt — derselbe Schnitt wie
* Ohne Abhängigkeiten, weil der Client sie lädt - derselbe Schnitt wie
* `lib/cases/terms.ts`. Die Beschriftungen stehen in `lib/i18n/cases.ts`.
*/
export const REACTIONS = ['gesehen', 'zustimmung', 'frage', 'wichtig'] as const
+1 -1
View File
@@ -4,7 +4,7 @@ import type { CasePreview } from '@/lib/chat/preview'
* Die Form, in der eine Nachricht die Grenze zur Anzeige überquert.
*
* Datumsangaben als Zeichenkette, weil ein `Date` diese Grenze nicht sauber
* überquert, und der Text einer zurückgezogenen Nachricht ist **leer** — nicht
* überquert, und der Text einer zurückgezogenen Nachricht ist **leer** - nicht
* ausgeblendet. Was der Client nicht bekommt, kann er nicht zeigen.
*/
export type ChatMessageView = {
+2 -2
View File
@@ -13,7 +13,7 @@ import type { Prisma } from '@afis/db'
*
* Fünf Fragen, die man morgens hat: woran war ich dran, woran arbeite ich, was
* ist fällig, wo hat jemand geschrieben, was hat sich getan. Jede läuft durch
* `visibleCaseWhere` — die Übersicht ist der Ort, an dem eine unachtsame
* `visibleCaseWhere` - die Übersicht ist der Ort, an dem eine unachtsame
* Abfrage am ehesten eine Akte verrät, die man nicht sehen darf.
*/
@@ -148,7 +148,7 @@ async function ungeleseneChats(p: EffectivePermissions): Promise<UnreadChat[]> {
if (orte.length === 0) return []
// **Eine** Abfrage statt einer je Ort. Vorher war das ein `count` pro Akte
// und pro Personalakte — bei hundert Orten hundert Hin- und Rückwege zur
// und pro Personalakte - bei hundert Orten hundert Hin- und Rückwege zur
// Datenbank, jeder für sich schnell und in der Summe die langsamste Stelle
// der Übersicht. Die Lesemarke steckt als Bedingung im ODER-Zweig des
// jeweiligen Ortes, gezählt wird über `groupBy`.
+2 -2
View File
@@ -18,7 +18,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
*
* Schmaler als der Aktenverlauf ist er trotzdem: keine Korrekturkette, keine
* Anhänge, keine Karenzzeit. Ein Abteilungsvermerk ist ein laufendes
* Protokoll, kein Beweismittel — die Ermittlung gehört in eine Fallakte.
* Protokoll, kein Beweismittel - die Ermittlung gehört in eine Fallakte.
*/
const AUSWAHL = {
@@ -101,7 +101,7 @@ export async function createEntry(
}
/**
* Ändern. Der vorherige Stand wird zur Version — in derselben Transaktion,
* Ändern. Der vorherige Stand wird zur Version - in derselben Transaktion,
* sonst gäbe es einen Moment, in dem der alte Text nirgends mehr steht.
*/
export async function editEntry(
+11 -11
View File
@@ -9,7 +9,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
import type { Prisma } from '@afis/db'
/**
* Abteilungen — die Ebene über der Personalakte.
* Abteilungen - die Ebene über der Personalakte.
*
* Der Aufbau ist derselbe wie bei der Personalakte, nur eine Stufe höher: was
* zu einer Abteilung aktenkundig ist, **wird abgeleitet und nicht
@@ -17,7 +17,7 @@ import type { Prisma } from '@afis/db'
* wenn eine beteiligte Person dieser Abteilung angehört.
*
* Das ist die wichtigste Festlegung hier. Eine eingetragene Abteilung an der
* Akte wäre schnell veraltet — Leute wechseln, Beteiligte kommen dazu — und
* Akte wäre schnell veraltet - Leute wechseln, Beteiligte kommen dazu - und
* niemand würde sie nachpflegen. Abgeleitet stimmt sie immer.
*
* Jede Abfrage läuft durch dieselben Sichtbarkeitsfragmente wie überall sonst.
@@ -31,7 +31,7 @@ export type DepartmentSummary = {
description: string | null
minClearance: number
isArchived: boolean
/** `null`, wenn der Fragende Personalakten gar nicht sehen darf. Nicht `0` —
/** `null`, wenn der Fragende Personalakten gar nicht sehen darf. Nicht `0` -
eine Null wäre die Auskunft „hier ist niemand", und die steht ihm nicht zu. */
memberCount: number | null
}
@@ -60,7 +60,7 @@ export async function listDepartments(
})
if (abteilungen.length === 0) return []
// Die Mitgliederzahl zählt nur, was der Nutzer auch sehen dürfte — sonst
// Die Mitgliederzahl zählt nur, was der Nutzer auch sehen dürfte - sonst
// verriete die Zahl, wie viele Personalakten über seiner Stufe liegen.
if (!darfPersonal(p)) {
return abteilungen.map((abteilung) => ({ ...abteilung, memberCount: null }))
@@ -116,7 +116,7 @@ export async function listMembers(p: EffectivePermissions, departmentId: string)
*
* Über die Beteiligten und deren Personalakten. Beide Richtungen zählen: ob
* jemand Ermittler oder Betroffener ist, ändert nichts daran, dass der Fall
* die Abteilung betrifft — für die Zugehörigkeit ist die Rolle unerheblich,
* die Abteilung betrifft - für die Zugehörigkeit ist die Rolle unerheblich,
* für den Zugriff nicht (siehe lib/cases/terms.ts).
*/
function caseOfDepartment(departmentId: string): Prisma.CaseWhereInput {
@@ -147,7 +147,7 @@ export async function listDepartmentCases(
}
/**
* Dokumente der Abteilung — abgeleitet wie alles hier.
* Dokumente der Abteilung - abgeleitet wie alles hier.
*
* Zwei Wege dorthin: das Dokument hängt an der Personalakte eines Mitglieds,
* oder an einer Fallakte, die diese Abteilung betrifft. Ein Dokument
@@ -190,7 +190,7 @@ export type Slice = { label: string; value: number }
* Womit diese Abteilung aktenkundig ist, nach Fallart.
*
* Gezählt wird über **dieselbe** Bedingung wie die Liste. Ein Diagramm, das
* mehr Fälle zeigt als die Liste hergibt, wäre selbst eine Auskunft — genau
* mehr Fälle zeigt als die Liste hergibt, wäre selbst eine Auskunft - genau
* wie ein Facettenzähler. Siehe harte Regel 6.
*/
export async function caseTypesOfDepartment(
@@ -211,7 +211,7 @@ export async function caseTypesOfDepartment(
/**
* Die Gegenrichtung: welche Abteilung wie viele Fälle hat.
*
* Das ist die Frage, mit der die Übersicht der Abteilungen anfängt — wo häuft
* Das ist die Frage, mit der die Übersicht der Abteilungen anfängt - wo häuft
* sich etwas. Auch hier zählt nur, was der Fragende sehen darf.
*/
export async function casesPerDepartment(
@@ -219,7 +219,7 @@ export async function casesPerDepartment(
redactedLabel: string,
): Promise<Slice[]> {
// **Alle** Abteilungen, auch die über der eigenen Stufe. Die Zahlen zählen
// weiterhin nur Fälle, die der Fragende sehen darf — was er hier erfährt,
// weiterhin nur Fälle, die der Fragende sehen darf - was er hier erfährt,
// ist also nicht, was in einer verborgenen Abteilung steht, sondern dass
// einige der ihm bekannten Fälle zu einer gehören, deren Namen er nicht
// kennt. Ein Diagramm, das sie stillschweigend weglässt, wäre die
@@ -281,7 +281,7 @@ export async function createDepartment(
input: { key: string; name: string; description?: string | null; minClearance: number },
): Promise<{ key: string }> {
if (!darfVerwalten(p)) throw new AccessError('no-permission')
// Eine Abteilung anzulegen, die man selbst nicht sieht, ergibt keinen Sinn —
// Eine Abteilung anzulegen, die man selbst nicht sieht, ergibt keinen Sinn -
// dieselbe Regel wie bei Akten und Dokumenten.
if (!p.isAdmin && input.minClearance > p.clearanceLevel) {
throw new AccessError('clearance-too-low')
@@ -367,7 +367,7 @@ export async function updateDepartment(
}
/**
* Eine Person einer Abteilung zuordnen — oder die Zuordnung aufheben.
* Eine Person einer Abteilung zuordnen - oder die Zuordnung aufheben.
*
* Setzt dasselbe Recht voraus wie das Ändern der Personalakte selbst: die
* Zugehörigkeit steht in der Akte, und wer sie nicht schreiben darf, darf auch
+2 -2
View File
@@ -18,8 +18,8 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
import type { Prisma } from '@afis/db'
/**
* Dokumente. Eine Ausfertigung aus einer Vorlage — Vorfallsbericht,
* Sonderarbeitsvertrag, Bußgeldbescheid — an einer Fallakte, an einer
* Dokumente. Eine Ausfertigung aus einer Vorlage - Vorfallsbericht,
* Sonderarbeitsvertrag, Bußgeldbescheid - an einer Fallakte, an einer
* Personalakte oder für sich stehend.
*
* Eine neue Dokumentart braucht hier keine Zeile: sie ist eine Vorlage mit
+1 -1
View File
@@ -3,7 +3,7 @@
*
* `code` ist ein Schlüssel in `de.actionErrors`, kein Text: die Meldung wird
* erst am Rand übersetzt. Ein Fehler, den der Nutzer nicht sehen darf, kommt
* gar nicht erst hier an — fehlender Zugriff endet in 404, nicht in 403.
* gar nicht erst hier an - fehlender Zugriff endet in 404, nicht in 403.
*/
export class AccessError extends Error {
constructor(readonly code: string) {
+1 -1
View File
@@ -9,7 +9,7 @@ import { createHash } from 'node:crypto'
* Dokument geben, das für eine höhere Stufe gerendert wurde.
*
* Der Stand (`updatedAt`) sorgt dafür, dass eine geänderte Akte neu gerendert
* wird — ein Export darf nie einen älteren Stand zeigen, als die Akte hat.
* wird - ein Export darf nie einen älteren Stand zeigen, als die Akte hat.
*/
export function exportCacheKey(input: {
kind: string
+1 -1
View File
@@ -6,7 +6,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
/**
* Vollständiger JSON-Abzug als Umzugs- und Notfallformat.
*
* **Ohne Stufenfilterung**, nur mit Recht `admin`, immer protokolliert — so
* **Ohne Stufenfilterung**, nur mit Recht `admin`, immer protokolliert - so
* steht es in docs/08-suche-und-export.md. Das ist die eine Stelle im System,
* an der die Klassifizierung nicht greift, und sie ist deshalb absichtlich
* unbequem: kein Knopf an einer Akte, sondern eine eigene Seite im
+1 -1
View File
@@ -5,7 +5,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
/**
* Die Druckansicht dient zwei Aufrufern: dem PDF-Dienst mit Zeichen und einem
* Menschen, der sie sich ansieht. Diese Funktion steht in einer eigenen Datei,
* weil sie `next-auth` lädt — der Zeichenpfad soll das nicht müssen.
* weil sie `next-auth` lädt - der Zeichenpfad soll das nicht müssen.
*/
export async function permissionsForPrint(
token: string | null | undefined,
+1 -1
View File
@@ -11,7 +11,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
*
* Die Stufe kommt dabei nicht aus dem Zeichen, sondern wird frisch aufgelöst
* und dann auf die im Zeichen vermerkte **gedeckelt**. Damit kann ein Zeichen
* nie mehr zeigen, als der Abrufer beim Anfragen sehen durfte — und auch nicht
* nie mehr zeigen, als der Abrufer beim Anfragen sehen durfte - und auch nicht
* mehr, als er jetzt noch darf, falls ihm inzwischen Rechte entzogen wurden.
*/
export async function permissionsFromRenderToken(
+4 -4
View File
@@ -18,14 +18,14 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
*
* 1. **Der Export respektiert die Stufe des Abrufers.** Die Druckansicht
* rendert mit seinen Rechten, gesperrte Felder erscheinen als Schwärzung
* mit Vermerk — nicht weggelassen, sonst merkt der Empfänger nicht, dass
* mit Vermerk - nicht weggelassen, sonst merkt der Empfänger nicht, dass
* er eine gekürzte Fassung hat.
* 2. **Wasserzeichen** auf jeder Seite: Name, Zeitstempel, Stufe.
* 3. **Jeder Export wird protokolliert**, ausnahmslos und mit Stufe,
* Zeitpunkt und Anlass.
*
* Wer exportieren darf, entscheidet allein die Freigabestufe: **mindestens die
* des Ziels.** Das ist strenger als das Lesen — wer als Beteiligter
* des Ziels.** Das ist strenger als das Lesen - wer als Beteiligter
* ausnahmsweise in eine höher eingestufte Akte darf, darf sie deshalb noch
* nicht ausser Haus geben. Und es ist einfacher als das frühere
* Vier-Augen-Verfahren, das ab Stufe 3 eine zweite Unterschrift verlangte:
@@ -38,7 +38,7 @@ export type ExportTarget =
| { kind: 'DOCUMENT'; documentNumber: string }
/**
* Legt einen Auftrag an — oder gibt einen fertigen zurück, wenn genau dieser
* Legt einen Auftrag an - oder gibt einen fertigen zurück, wenn genau dieser
* Stand schon einmal für diese Stufe gerendert wurde.
*/
export async function requestExport(
@@ -71,7 +71,7 @@ export async function requestExport(
return { jobId: fertig.id, status: 'READY' }
}
// Läuft schon einer für denselben Stand? Dann daran hängen — aber der
// Läuft schon einer für denselben Stand? Dann daran hängen - aber der
// Protokolleintrag entsteht trotzdem. „Jeder Export wird protokolliert"
// heisst jeder Abruf, nicht jeder Rendervorgang: wer sich an einen fremden
// Auftrag hängt, bekommt dieselbe Datei und muss genauso darin stehen.
+1 -1
View File
@@ -35,7 +35,7 @@ export function createRenderToken(claim: Omit<RenderClaim, 'expiresAt'>): string
}
/**
* Prüft das Zeichen. Gibt `null` zurück, wenn irgendetwas nicht stimmt — der
* Prüft das Zeichen. Gibt `null` zurück, wenn irgendetwas nicht stimmt - der
* Aufrufer macht daraus 404, nicht 403: dass es die Druckansicht gibt, geht
* niemanden etwas an, der kein gültiges Zeichen hat.
*/
+1 -1
View File
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'
* Ein winziger ZIP-Schreiber, nur „gespeichert" ohne Kompression.
*
* Eigener Code statt einer Abhängigkeit, weil der Inhalt eines Sammelexports
* PDFs und bereits neu kodierte Bilder sind — beides ist komprimiert, ein
* PDFs und bereits neu kodierte Bilder sind - beides ist komprimiert, ein
* Deflate darüber bringt nichts und kostet nur CPU auf einem Heimserver. Was
* bleibt, ist das Behälterformat, und das sind achtzig Zeilen.
*
+1 -1
View File
@@ -5,7 +5,7 @@ import net from 'node:net'
*
* Es wird **INSTREAM** benutzt, nicht SCAN mit einem Pfad: dann muss der
* Scanner die Datei nicht sehen können, und die Anbindung funktioniert auch,
* wenn clamd anderswo läuft. Das Protokoll ist simpel — Länge als 4 Byte,
* wenn clamd anderswo läuft. Das Protokoll ist simpel - Länge als 4 Byte,
* Nutzdaten, dann eine Null-Länge als Abschluss.
*/
+1 -1
View File
@@ -2,7 +2,7 @@
* Grenzen für Uploads, aus docs/07-dateien-und-sicherheit.md.
*
* Ohne Abhängigkeiten, damit die Oberfläche dieselben Zahlen anzeigen kann,
* die der Server durchsetzt — geprüft wird ausschliesslich serverseitig.
* die der Server durchsetzt - geprüft wird ausschliesslich serverseitig.
*/
/** 25 MB pro Datei. */
+4 -4
View File
@@ -7,7 +7,7 @@
* Bewusst eine eigene Signaturtabelle statt `file-type`: die Liste der
* erlaubten Typen ist kurz und geschlossen, und was nicht darin steht, wird
* abgelehnt. Eine Bibliothek, die hundert Formate erkennt, hilft bei einer
* Positivliste nicht — sie erweitert nur die Angriffsfläche und die Menge
* Positivliste nicht - sie erweitert nur die Angriffsfläche und die Menge
* dessen, was aktuell gehalten werden muss. Der eigentliche Schutz ist ohnehin
* die Neukodierung danach, siehe `process.ts`.
*
@@ -67,7 +67,7 @@ export type Detection =
*
* Reine Textdateien haben keine Signatur. Sie werden deshalb zuletzt geprüft
* und nur dann angenommen, wenn der ganze Inhalt gültiges UTF-8 ohne
* Steuerzeichen ist — sonst wäre `text/plain` das Schlupfloch, durch das jedes
* Steuerzeichen ist - sonst wäre `text/plain` das Schlupfloch, durch das jedes
* Binärformat passt.
*/
export function detectType(content: Uint8Array): Detection {
@@ -106,8 +106,8 @@ export function looksLikeText(content: Uint8Array): boolean {
/**
* Der gemeldete Typ muss zum erkannten passen.
*
* Nicht, weil der gemeldete etwas wert wäre — er kommt vom Browser und ist frei
* wählbar —, sondern weil eine Abweichung ein Hinweis ist: wer eine PNG-Datei
* Nicht, weil der gemeldete etwas wert wäre - er kommt vom Browser und ist frei
* wählbar -, sondern weil eine Abweichung ein Hinweis ist: wer eine PNG-Datei
* hochlädt und sie als PDF ankündigt, hat etwas vor.
*/
export function matchesDeclared(detected: AllowedType, declared: string | null): boolean {
+1 -1
View File
@@ -11,7 +11,7 @@ import { getEnv } from '@/lib/env'
* Ausgeliefert wird ausschliesslich aus `store/`. Siehe docs/07.
*
* Der Dateiname ist die Kennung des Anhangs, nie der Name, den der Nutzer
* mitgeschickt hat — der steht in der Datenbank und wird erst beim Ausliefern
* mitgeschickt hat - der steht in der Datenbank und wird erst beim Ausliefern
* wieder gesetzt. Ein Pfad, der aus einer Eingabe entsteht, ist eine
* Einladung.
*/
+3 -3
View File
@@ -26,7 +26,7 @@ export async function runCheckChain(
/**
* Nur für die bewusste Freigabe durch einen Administrator, wenn der
* Scanner nicht erreichbar war. Die Typprüfung und die Neukodierung laufen
* auch dann — sie sind der eigentliche Schutz.
* auch dann - sie sind der eigentliche Schutz.
*/
skipScan?: boolean
} = {},
@@ -157,8 +157,8 @@ async function freigeben(
// Gleicher Inhalt schon da: nur verweisen, nicht ein zweites Mal ablegen.
//
// Der Verweis gilt aber nur, wenn die Datei auch wirklich noch liegt. Eine
// Zeile in der Datenbank ist kein Beweis dafür: geht die Ablage verloren —
// ein geleertes Verzeichnis, ein nicht eingehängtes Volume —, zeigten sonst
// Zeile in der Datenbank ist kein Beweis dafür: geht die Ablage verloren -
// ein geleertes Verzeichnis, ein nicht eingehängtes Volume -, zeigten sonst
// alle künftigen Uploads desselben Inhalts stumm ins Leere.
const vorhanden = await db.attachment.findFirst({
where: { sha256, scanStatus: 'CLEAN', id: { not: attachmentId } },
+2 -2
View File
@@ -16,7 +16,7 @@ const run = promisify(execFile)
* Das ist die wirksamste Massnahme der ganzen Kette. Ein Bild, das durch
* `sharp` gelaufen ist, besteht nur noch aus Pixeln: EXIF, Farbprofile,
* angehängte Nutzlast und alles, was ein Betrachter versehentlich auswerten
* könnte, sind weg — nicht gefiltert, sondern nie übernommen.
* könnte, sind weg - nicht gefiltert, sondern nie übernommen.
* Siehe docs/07-dateien-und-sicherheit.md.
*/
@@ -126,7 +126,7 @@ function reencodeText(content: Uint8Array): ReencodeResult {
*
* `--empty --pages datei 1-z --` baut das Dokument aus den Seiten neu auf.
* Der ursprüngliche Dokumentkatalog wird dabei nicht übernommen, und mit ihm
* verschwinden `OpenAction`, eingebettetes JavaScript und angehängte Dateien —
* verschwinden `OpenAction`, eingebettetes JavaScript und angehängte Dateien -
* die drei Wege, auf denen ein PDF etwas tut, statt nur dazustehen.
*
* Fehlt `qpdf`, wird die Datei **nicht** durchgewinkt. Sie bleibt in der
+5 -5
View File
@@ -91,7 +91,7 @@ async function aufloesen(
*
* Der Datensatz entsteht **sofort** mit `PENDING`, damit die Oberfläche ihn
* zeigen kann, während die Prüfung läuft. Der gemeldete Typ wandert
* unverändert in `mimeType` und wird von der Prüfkette überschrieben — bis
* unverändert in `mimeType` und wird von der Prüfkette überschrieben - bis
* dahin ist er nichts als eine Behauptung des Browsers.
*/
export async function acceptUpload(
@@ -183,7 +183,7 @@ export async function listAttachments(
* Lädt einen Anhang zum Ausliefern.
*
* Nur was die Prüfkette bestanden hat oder von einem Administrator bewusst
* freigegeben wurde. Alles andere gibt es für die Auslieferung nicht — auch
* freigegeben wurde. Alles andere gibt es für die Auslieferung nicht - auch
* nicht mit Recht `admin`, denn die Datei liegt dann noch in der Quarantäne.
*/
export async function loadDeliverable(p: EffectivePermissions, id: string) {
@@ -207,7 +207,7 @@ export async function loadDeliverable(p: EffectivePermissions, id: string) {
/**
* Gibt eine Datei frei, deren Prüfung nicht abgeschlossen werden konnte.
*
* Nur mit Recht `admin` und nur aus `FAILED` — was als Schadsoftware oder
* Nur mit Recht `admin` und nur aus `FAILED` - was als Schadsoftware oder
* unzulässiger Typ erkannt wurde, bleibt blockiert. Die Freigabe wiederholt
* die Kette ohne den Virenscanner; die Typprüfung läuft dabei **immer** mit.
*/
@@ -241,7 +241,7 @@ export async function releaseAttachment(
})
// Die Kette noch einmal, ohne den Scanner. Typprüfung und Neukodierung
// laufen dabei mit — die Freigabe überspringt nur das, was ausgefallen war.
// laufen dabei mit - die Freigabe überspringt nur das, was ausgefallen war.
await runCheckChain(id, { skipScan: true })
}
@@ -249,7 +249,7 @@ export async function releaseAttachment(
* Löst die Lichtbildfelder eines Dokuments zu Adressen auf.
*
* **Serverseitig**, und nur für Anhänge, die die Prüfkette bestanden haben und
* die der Leser sehen darf. Der Browser bekommt eine Adresse oder gar nichts —
* die der Leser sehen darf. Der Browser bekommt eine Adresse oder gar nichts -
* nie eine Kennung, zu der er selbst probieren müsste.
*/
export async function resolvePhotoUrls(
+1 -1
View File
@@ -4,7 +4,7 @@ import type { AttachmentView } from '@/components/files/Attachments'
* Formt die Datenbankzeilen für die Oberfläche um.
*
* Zwei Dinge passieren hier, beide mit Absicht: `thumbKey` wird zu einem
* Wahrheitswert — der Pfad auf der Platte geht den Browser nichts an —, und
* Wahrheitswert - der Pfad auf der Platte geht den Browser nichts an -, und
* `createdAt` wird zu einer Zeichenkette, weil ein `Date` die Grenze zur
* Client-Komponente nicht sauber überquert.
*/
+2 -2
View File
@@ -110,7 +110,7 @@ export const adminText = {
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
@@ -196,7 +196,7 @@ export const settingsText = {
export const vitalsText = {
heading: 'Ladewerte',
intro:
'Gemessen in den Browsern der Nutzer, letzte sieben Tage. Nur Zahlen und das Routenmuster — wer wann welche Akte geöffnet hat, steht im Zugriffsprotokoll.',
'Gemessen in den Browsern der Nutzer, letzte sieben Tage. Nur Zahlen und das Routenmuster - wer wann welche Akte geöffnet hat, steht im Zugriffsprotokoll.',
empty: 'Noch nichts gemessen.',
metric: 'Wert',
median: 'Median',
+2 -2
View File
@@ -3,7 +3,7 @@
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
@@ -65,7 +65,7 @@ export const caseText = {
} as Record<string, string>,
/**
* Was die Zustände bedeuten. Sie stehen im Lebenszyklus in
* docs/05-akten-und-vorlagen.md, standen aber nirgends in der Oberfläche —
* docs/05-akten-und-vorlagen.md, standen aber nirgends in der Oberfläche -
* und ein Filter, dessen Bezeichnung man nicht versteht, ist keiner.
*/
statusHints: {
+1 -1
View File
@@ -36,7 +36,7 @@ export const chatText = {
} as Record<string, string>,
// Zeichen statt Emoji: sie drucken, sie lesen sich im schmalen Schnitt und
// sie brauchen keine Schriftart, die es vielleicht nicht gibt. Für "wichtig"
// ein Dreieck und kein Ausrufezeichen — die Oberfläche ruft nicht.
// ein Dreieck und kein Ausrufezeichen - die Oberfläche ruft nicht.
reactionSymbols: {
gesehen: '✓',
zustimmung: '+',
+2 -2
View File
@@ -3,7 +3,7 @@
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
@@ -23,7 +23,7 @@ export const commonText = {
/**
* Meldungen zu abgelehnten Aktionen. Der Schlüssel ist der `code` eines
* `AccessError`, siehe lib/errors.ts. Gilt für Fallakten, Personalakten und
* Dokumente gleichermaßen — die Regeln sind dieselben.
* Dokumente gleichermaßen - die Regeln sind dieselben.
*/
export const actionErrorsText = {
'not-found': 'Die Akte gibt es nicht, oder du darfst sie nicht sehen.',
+1 -1
View File
@@ -11,7 +11,7 @@
*
* Die Texte stehen nach Bereichen getrennt daneben; diese Datei setzt sie nur
* wieder zusammen. Serverkomponenten duerfen `de` nehmen. Eine
* Client-Komponente importiert **den Bereich, den sie braucht** — sonst traegt
* Client-Komponente importiert **den Bereich, den sie braucht** - sonst traegt
* sie die Texte der ganzen Anwendung ins Buendel.
*/
import {
+1 -1
View File
@@ -25,7 +25,7 @@ export const departmentText = {
minClearance: 'Mindeststufe',
archived: 'Archiviert',
derivedHint:
'Eine Fallakte gehört zu dieser Abteilung, wenn eine beteiligte Person ihr angehört. Die Zuordnung wird nicht eingetragen, sondern ergibt sich — damit stimmt sie auch, wenn sich die Beteiligten ändern.',
'Eine Fallakte gehört zu dieser Abteilung, wenn eine beteiligte Person ihr angehört. Die Zuordnung wird nicht eingetragen, sondern ergibt sich - damit stimmt sie auch, wenn sich die Beteiligten ändern.',
} as const
export const departmentAdminText = {
+1 -1
View File
@@ -3,7 +3,7 @@
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
+1 -1
View File
@@ -2,7 +2,7 @@
* Export und Druck.
*
* Eigene Datei und nicht bei den Dokumenten: die Fallakte lädt die
* Dokumenttexte sofort, den Export nie — er hat eine eigene Seite. Lägen sie
* Dokumenttexte sofort, den Export nie - er hat eine eigene Seite. Lägen sie
* zusammen, käme er doch mit. Siehe docs/13-entscheidungen.md.
*/
export const exportText = {
+1 -1
View File
@@ -3,7 +3,7 @@
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
+3 -3
View File
@@ -3,7 +3,7 @@
*
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
* Ansicht die der ganzen Anwendung mit sich herum - das Budget aus
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
@@ -115,7 +115,7 @@ export const healthText = {
export const notifyText = {
heading: 'Benachrichtigungen',
intro:
'Discord erfährt nie den Inhalt einer Akte. Bei Akten ab Stufe 3 auch nicht den Titel — nur, dass sich etwas getan hat, und einen Link.',
'Discord erfährt nie den Inhalt einer Akte. Bei Akten ab Stufe 3 auch nicht den Titel - nur, dass sich etwas getan hat, und einen Link.',
mode: 'Was soll dich erreichen?',
save: 'Übernehmen',
saved: 'Übernommen',
@@ -175,7 +175,7 @@ export const paletteText = {
} as Record<string, string>,
/**
* Die Sprungziele der Palette. Sie stehen hier und nicht in der Komponente,
* weil sie Oberflächentext sind — die Adressen daneben sind es nicht.
* weil sie Oberflächentext sind - die Adressen daneben sind es nicht.
*/
pages: [
{ href: '/uebersicht', label: 'Übersicht' },
+3 -3
View File
@@ -13,7 +13,7 @@ import type { Prisma } from '@afis/db'
* in dieser Anwendung, und sie im Worker ein zweites Mal zu bauen hiesse, sie
* zweimal richtig haben zu müssen.
*
* Deshalb steht in `meta` nur, was **stehen darf** — fertig entschieden. Ein
* Deshalb steht in `meta` nur, was **stehen darf** - fertig entschieden. Ein
* Titel landet dort nur, wenn er auch nach Discord darf. Der Worker setzt
* daraus einen Satz zusammen und hat keine Entscheidung mehr zu treffen.
* Siehe docs/06-chat-und-realtime.md.
@@ -27,7 +27,7 @@ export type OutboxKind = 'entry.new' | 'chat.new' | 'chat.mention' | 'case.statu
/**
* Die Empfänger einer Meldung zu einer Akte.
*
* Wer die Akte nicht sehen darf, bekommt nichts — auch keinen Hinweis, dass es
* Wer die Akte nicht sehen darf, bekommt nichts - auch keinen Hinweis, dass es
* sie gibt. Geprüft wird mit **derselben** Abfrage wie beim Lesen:
* `visibleCaseWhere`, je Empfänger. Das ist eine Abfrage pro Kandidat und bei
* dieser Nutzerzahl billiger als der Fehler, die Regel ein zweites Mal zu
@@ -136,7 +136,7 @@ export async function enqueueCaseNotification(
* Link führt in die Anwendung, und die prüft selbst.
*
* Geprüft wird je Empfänger mit derselben Abfrage wie beim Lesen, plus dem
* Recht `personnel.read` — ohne das gibt es die Akte für ihn nicht.
* Recht `personnel.read` - ohne das gibt es die Akte für ihn nicht.
*/
export async function enqueuePersonnelNotification(
tx: Prisma.TransactionClient,
+2 -2
View File
@@ -217,7 +217,7 @@ export async function reclassifyPersonnelFile(
* Das ist der häufigste Ort, an dem ein Aktensystem leckt: eine Personalakte
* der Stufe 1, auf der „3 Fälle" steht, verrät die Existenz von Ermittlungen,
* die der Leser nicht sehen darf. Deshalb steckt der Filter in der Abfrage und
* die Zählung entsteht aus dem gefilterten Ergebnis — nie aus einem eigenen
* die Zählung entsteht aus dem gefilterten Ergebnis - nie aus einem eigenen
* `count` ohne Filter. Siehe docs/05-akten-und-vorlagen.md, Rückverweise.
*/
export async function loadBacklinks(
@@ -295,7 +295,7 @@ export async function listStatusTags() {
/**
* Die AfiS-Mitglieder, die an dieser Personalakte hängen.
*
* Wer hier steht, liest die Akte unabhängig von seiner Freigabestufe — das ist
* Wer hier steht, liest die Akte unabhängig von seiner Freigabestufe - das ist
* derselbe ausnahmsweise Zugang wie `CaseParticipant` bei der Fallakte.
*/
export async function listAssignments(p: EffectivePermissions, personnelFileId: string) {
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Feste Wertelisten der Personalakte.
*
* **Ohne Abhängigkeiten**, weil der Client sie lädt — derselbe Schnitt wie
* **Ohne Abhängigkeiten**, weil der Client sie lädt - derselbe Schnitt wie
* `lib/cases/terms.ts`. Die Status-Etiketten stehen bewusst **nicht** hier:
* sie sind im Adminbereich pflegbar und kommen aus der Datenbank.
*/
+6 -6
View File
@@ -5,7 +5,7 @@ import type { EffectivePermissions } from '@/lib/auth/permissions'
import type { RecentTarget } from '@afis/db'
/**
* „Zuletzt geöffnet" — die persönliche Sprungliste.
* „Zuletzt geöffnet" - die persönliche Sprungliste.
*
* Sie beantwortet eine Frage, die man oft hat: *woran war ich gerade dran?*
* Und sie beantwortet ausdrücklich **keine** andere. Drei Dinge halten sie auf
@@ -15,7 +15,7 @@ import type { RecentTarget } from '@afis/db'
* nächsten Öffnen überschrieben wird. Aus dieser Tabelle lässt sich nicht
* ablesen, wie oft oder in welcher Reihenfolge jemand gearbeitet hat.
* 2. **Nur die eigene.** Es gibt in der Anwendung keinen Weg, die Liste eines
* anderen zu lesen — keine Ansicht, kein Adminbereich, keine Funktion mit
* anderen zu lesen - keine Ansicht, kein Adminbereich, keine Funktion mit
* fremder Kennung als Parameter. Jede Abfrage hier nimmt `p.userId`.
* 3. **Jederzeit löschbar**, ohne Rückfrage und ohne Spur.
*
@@ -30,7 +30,7 @@ export const RECENT_MAX = 20
/**
* Einen Sprung vormerken.
*
* Nur aufrufen, wenn der Nutzer den Datensatz auch wirklich sehen durfte —
* Nur aufrufen, wenn der Nutzer den Datensatz auch wirklich sehen durfte -
* die Aufrufer stehen hinter `loadVisible…` und 404.
*/
export async function noteOpened(
@@ -71,7 +71,7 @@ export type RecentEntry = {
targetType: RecentTarget
targetId: string
href: string
/** Aktenzeichen, Dokumentnummer oder Name — das, was man wiedererkennt. */
/** Aktenzeichen, Dokumentnummer oder Name - das, was man wiedererkennt. */
label: string
/** Titel der Akte, Dienstgrad der Person. Kann fehlen. */
secondary?: string
@@ -85,7 +85,7 @@ export type RecentEntry = {
* Die vorgemerkten Kennungen sind für sich genommen wertlos: jede wird durch
* dieselbe Sichtbarkeitsprüfung geholt wie überall sonst. Steigt eine Akte
* nachträglich über die Freigabestufe des Nutzers, verschwindet sie aus der
* Liste, ohne dass jemand aufräumen müsste — genau wie bei der
* Liste, ohne dass jemand aufräumen müsste - genau wie bei der
* Beobachtungsliste.
*/
export async function listRecent(p: EffectivePermissions, take = 8): Promise<RecentEntry[]> {
@@ -165,7 +165,7 @@ export async function listRecent(p: EffectivePermissions, take = 8): Promise<Rec
for (const zeile of zeilen) {
const gefunden = treffer.get(`${zeile.targetType}:${zeile.targetId}`)
// Nicht gefunden heisst: nicht mehr sichtbar. Kein Platzhalter, keine
// Lücke, kein Hinweis — die Existenz ist selbst eine Information.
// Lücke, kein Hinweis - die Existenz ist selbst eine Information.
if (!gefunden) continue
liste.push({ ...gefunden, openedAt: zeile.openedAt })
if (liste.length >= take) break
+1 -1
View File
@@ -10,7 +10,7 @@ import type { RecentTarget } from '@afis/db'
*
* **Vorabrufe zählen nicht.** Next holt die Seiten hinter sichtbaren Links im
* Voraus. Ohne diese Prüfung stünde in der Liste, worüber jemand mit der Maus
* gefahren ist — die Übersicht allein würde sie mit zehn Akten füllen, die
* gefahren ist - die Übersicht allein würde sie mit zehn Akten füllen, die
* niemand geöffnet hat. Das wäre nicht nur falsch, es wäre die unangenehmere
* Sorte falsch: eine Aufzeichnung von Aufmerksamkeit statt von Handlung.
*
+2 -2
View File
@@ -7,7 +7,7 @@
* Der Hinweis kam von [[c4]]Wachmann Perreira, Sektor B[[/c4]] am 27.08.
*
* Der Server ersetzt den Inhalt für niedrigere Stufen. **Der echte Wert
* verlässt den Server nicht** — deshalb gibt diese Datei Segmente zurück und
* verlässt den Server nicht** - deshalb gibt diese Datei Segmente zurück und
* nicht etwa den vollen Text mit einem Sichtbarkeits-Flag. Was der Client
* ausblenden müsste, hat er schon.
*
@@ -94,7 +94,7 @@ export function stripMarkup(source: string): string {
/**
* Entfernt ausgezeichnete Bereiche samt Inhalt. Genau das tut auch die
* Funktion `strip_redactions` in der Migration, bevor der Suchvektor gefüllt
* wird — beide müssen dasselbe Ergebnis liefern.
* wird - beide müssen dasselbe Ergebnis liefern.
*/
export function stripRedacted(source: string): string {
return source.replace(MARKUP, ' ')
+1 -1
View File
@@ -3,7 +3,7 @@
*
* Ohne Abhängigkeiten: die Suchseite baut damit ihre Adresse, und gespeicherte
* Ansichten sind nichts anderes als eine solche Adresse. Dass beides dieselbe
* Darstellung benutzt, ist der Punkt — eine Ansicht ist ein Lesezeichen.
* Darstellung benutzt, ist der Punkt - eine Ansicht ist ein Lesezeichen.
*/
/** Die Arten, über die einzeln gesucht werden kann. */
+9 -9
View File
@@ -11,17 +11,17 @@ import type { Prisma } from '@afis/db'
*
* Zwei Schritte, und der Unterschied ist wichtig:
*
* 1. **Was passt** — eine Abfrage auf `SearchIndex`, in der Zeile zur Stufe
* 1. **Was passt** - eine Abfrage auf `SearchIndex`, in der Zeile zur Stufe
* des Suchenden. Sie kann nichts liefern, was er nicht lesen darf, weil
* höher eingestufte Inhalte gar nicht erst in dieser Zeile stehen.
* 2. **Was er sehen darf und wie viele es sind** — eine Abfrage mit
* 2. **Was er sehen darf und wie viele es sind** - eine Abfrage mit
* `visibleCaseWhere` und den Filtern. Treffer, Trefferzahl und die
* Facettenzähler entstehen dort **gemeinsam**, damit sich aus einer Lücke
* nichts ableiten lässt. Siehe docs/08-suche-und-export.md.
*
* Der erste Schritt ist die Suchbedingung, nicht die Zugriffsprüfung. Die
* steht ausschliesslich im zweiten und stammt aus derselben Funktion wie in
* der Fallliste — es gibt keine zweite Fassung davon.
* der Fallliste - es gibt keine zweite Fassung davon.
*/
/** So viele Treffer holt der Volltext höchstens, nach Rang sortiert. */
@@ -40,7 +40,7 @@ export const PREVIEW_SIZE = 5
* Personalakten setzen neben der Freigabestufe ein eigenes Recht voraus.
*
* Das stand in der Kommandopalette und auf der Aktenseite, fehlte aber in der
* Suche — ein Nutzer mit hoher Stufe und ohne `personnel.read` konnte sich
* Suche - ein Nutzer mit hoher Stufe und ohne `personnel.read` konnte sich
* über `/suche?kind=personnel` eine Liste aller Personalakten holen. Die
* Prüfung gehört an **jede** Stelle, die diese Akten herausgibt, und deshalb
* in diese Datei und nicht in die Seite.
@@ -57,7 +57,7 @@ async function matchingIds(
clearanceLevel: number,
): Promise<string[] | null> {
const begriff = q.trim()
// Ohne Suchbegriff filtert die Seite nur — dann gibt es keine Vorauswahl.
// Ohne Suchbegriff filtert die Seite nur - dann gibt es keine Vorauswahl.
if (begriff.length === 0) return null
const rows = await db.$queryRaw<{ targetId: string }[]>`
@@ -233,7 +233,7 @@ export async function searchPersonnel(
filters: SearchFilters,
options: { take?: number } = {},
) {
// Ohne das Recht gibt es diese Akten für ihn nicht — auch nicht als Zahl.
// Ohne das Recht gibt es diese Akten für ihn nicht - auch nicht als Zahl.
if (!darfPersonalakten(p)) {
return { hits: [], total: 0, truncated: false }
}
@@ -285,9 +285,9 @@ function zuFacet(rows: { _count: { _all: number } }[], key: string): Facet[] {
*
* Die Zahlen entstehen mit **denselben** Bedingungen wie die Trefferlisten.
* Eine Zahl, die mehr zählt als die Liste hergibt, wäre selbst eine
* Information — siehe harte Regel 6.
* Information - siehe harte Regel 6.
*
* `null` heisst: diese Art gibt es für diesen Nutzer nicht. Nicht `0` — eine
* `null` heisst: diese Art gibt es für diesen Nutzer nicht. Nicht `0` - eine
* Null wäre die Auskunft „hier ist nichts", und die steht ihm nicht zu.
*/
export async function searchCounts(
@@ -316,7 +316,7 @@ export async function searchCounts(
*
* Seit die Beteiligten im Index stehen, findet die Suche nach „Jonathan
* Fletcher" auch Akten, in denen der Name nirgends im Text vorkommt. Ohne
* einen Hinweis sieht das aus wie ein Fehlgriff — die Trefferzeile zeigt
* einen Hinweis sieht das aus wie ein Fehlgriff - die Trefferzeile zeigt
* deshalb, in welcher Rolle die Person dort steht.
*
* Gesucht wird nur unter den Akten, die als Treffer ohnehin schon durch die
+2 -2
View File
@@ -6,7 +6,7 @@ import { toSearchParams } from '@/lib/search/query'
* Sie stehen im Programm und nicht in der Datenbank, weil sie von der
* **Gegenwart** abhängen: „diese Woche" und „seit 14 Tagen" wären als
* gespeicherte Zeitpunkte schon am nächsten Tag falsch. Selbst angelegte
* Ansichten sind dagegen genau das — ein Lesezeichen auf eine Adresse.
* Ansichten sind dagegen genau das - ein Lesezeichen auf eine Adresse.
*/
export type BuiltinView = { key: string; params: string }
@@ -33,7 +33,7 @@ export function builtinViews(now: Date = new Date()): BuiltinView[] {
/**
* „Seit deinem letzten Besuch geändert" braucht die Lesemarke und steht
* deshalb nicht in der Liste oben — die Seite ergänzt sie, wenn es eine gibt.
* deshalb nicht in der Liste oben - die Seite ergänzt sie, wenn es eine gibt.
*/
export function sinceLastVisitView(lastReadAt: Date): BuiltinView {
return { key: 'sinceLastVisit', params: toSearchParams({ changedSince: lastReadAt.toISOString() }) }
+1 -1
View File
@@ -49,7 +49,7 @@ export function asJson(value: unknown): Prisma.InputJsonValue {
* Wendet die Stufe des Lesers auf die Abschnitte an.
*
* Felder über seiner Stufe werden **hier**, also serverseitig, durch einen
* Platzhalter ersetzt. Der echte Wert verlässt den Server nicht — nie im
* Platzhalter ersetzt. Der echte Wert verlässt den Server nicht - nie im
* Client ausblenden. Dasselbe gilt für Schwärzungen im Fließtext, die als
* Segmente zurückkommen.
*/
+6 -6
View File
@@ -1,10 +1,10 @@
/**
* Bausteine einer Vorlage — Typen und reine Funktionen.
* Bausteine einer Vorlage - Typen und reine Funktionen.
*
* **Ohne zod.** Dieses Modul wird auch im Client geladen (Vorlagen-Editor,
* Vorschau), und eine Validierungsbibliothek im Bündel kostet mehr als das
* Budget aus docs/09-performance-und-ux.md hergibt. Die verbindliche Prüfung
* steht in `schema.ts` und läuft ausschliesslich serverseitig — dort, wo sie
* steht in `schema.ts` und läuft ausschliesslich serverseitig - dort, wo sie
* ohnehin hingehört.
*
* Eine Vorlage ist eine **geordnete Liste von Bausteinen**. Manche nehmen eine
@@ -54,7 +54,7 @@ export type InputField = {
rows?: number
/**
* Statt einer eigenen Zelle im Raster: dieses Feld füllt einen festen Platz
* des Aktenblatts. Bekannt ist `classbar` — der Klassifizierungsbalken.
* des Aktenblatts. Bekannt ist `classbar` - der Klassifizierungsbalken.
*/
slot?: string
/** Hebt den Wert hervor, etwa die Freigabestufen im Aktenblatt. */
@@ -94,7 +94,7 @@ export function fillPlaceholders(
// **Nur eigene Eigenschaften.** `values[name]` lief über die
// Prototypkette: ein Baustein mit dem Platzhalter `{{constructor}}` oder
// `{{toString}}` bekam den Quelltext der geerbten Funktion in den Text
// geschrieben — die Feldschlüssel sind Kleinbuchstaben, und `constructor`
// geschrieben - die Feldschlüssel sind Kleinbuchstaben, und `constructor`
// ist einer. Kein Code, der ausgeführt wird, aber Innenleben, das in einem
// Dokument nichts zu suchen hat.
const value = Object.hasOwn(values, name) ? values[name] : undefined
@@ -119,7 +119,7 @@ export const DOSSIER_COLUMNS = 5
/**
* Zerlegt die Bausteine in die Zeilen des Aktenblatts.
*
* Solange die Lichtbildzelle mitläuft, ist eine Zeile eine Spalte schmaler —
* Solange die Lichtbildzelle mitläuft, ist eine Zeile eine Spalte schmaler -
* genau so, wie das Formular es tut. Danach nimmt sie die volle Breite.
* Felder, die in einen festen Platz gehören oder von einer Unterschrift
* mitgezeichnet werden, tauchen im Raster nicht auf.
@@ -184,7 +184,7 @@ export function signatureDateKeys(fields: TemplateField[]): Set<string> {
/**
* Grobe Formprüfung für die Vorschau im Editor. Sagt, ob sich die Eingabe
* überhaupt darstellen lässt — nicht, ob sie gespeichert werden darf. Das
* überhaupt darstellen lässt - nicht, ob sie gespeichert werden darf. Das
* entscheidet allein der Server.
*/
export function looksLikeFields(value: unknown): value is TemplateField[] {
+1 -1
View File
@@ -25,7 +25,7 @@ const gemeinsam = {
/**
* `.strict()` und nicht die Voreinstellung: zod wuerde unbekannte Schluessel
* **stillschweigend verwerfen**. Fuer eine Vorlage, die von Hand als JSON
* eingegeben wird, ist das die schlechtere Antwort — wer `typ` statt `type`
* eingegeben wird, ist das die schlechtere Antwort - wer `typ` statt `type`
* schreibt, bekaeme eine gespeicherte Vorlage, die nicht tut, was dasteht.
* Und was nicht ankommt, kann auch nichts einschleusen.
*/
+1 -1
View File
@@ -6,7 +6,7 @@
* steht so ausdrücklich hier statt in fremdem Code.
*
* Die Eigenschaft ist: **die Kennungen sind lexikografisch sortierbar wie die
* Zeit.** Darauf beruhen zwei Dinge — die Nachlieferung beim Wiederverbinden
* Zeit.** Darauf beruhen zwei Dinge - die Nachlieferung beim Wiederverbinden
* (`id > seit`) und die Reihenfolge im Chat, ohne dass eine Sortierspalte
* nötig wäre. Siehe docs/06-chat-und-realtime.md.
*