Files
AfiS-Aktensystem/web/lib/i18n/shell.ts
T
Harlekin 89edaa6ae1 Protokoll im Adminbereich, ein Knopf für das ganze Blatt
Die restlichen zwei Änderungswünsche.

**Protokoll.** „Wer was wo macht" stand seit dem ersten Tag in AuditLog -
harte Regel 3 sorgt dafür. Was fehlte, war die Ansicht darauf. /admin/protokoll
ist genau das und keine zweite Aufzeichnung: filterbar nach Person, Aktion,
Art und Zeitraum, rückwärts blätternd.

Nur für die Leitung. Eine Zeile nennt Aktenzeichen und Namen, und schon die
Existenz einer Zeile verrät, dass es einen Vorgang gibt; ein gefiltertes
Protokoll für alle wäre ein Weg, die Sichtbarkeitsregeln von hinten
aufzurollen. Aus demselben Grund werden Kennungen ohne Sichtbarkeitsfilter zu
Aktenzeichen aufgelöst - ein Protokoll aus UUIDs liest niemand, und eines, das
niemand liest, ist keine Kontrolle.

**Speichern.** Jedes Feld trug seinen eigenen Knopf: ein Arbeitsvertrag mit
zwölf Feldern hiess zwölfmal speichern. Der Knopf steht jetzt am Blatt.

Geschrieben wird trotzdem je Feld und nicht als Transaktion, und das folgt aus
dem Datenmodell: Abschnitte sind einzeln versioniert, weil mehrere Leute
gleichzeitig an einer Akte arbeiten. Als Transaktion blockierte ein einziges
zwischenzeitlich geändertes Feld das ganze Blatt - elf gute Änderungen verloren,
weil jemand das zwölfte angefasst hat. So geht durch, was durchgeht, und die
Rückmeldung sagt, was offen blieb und wo. Lichtbilder bleiben ausgenommen: ein
Upload mit Prüfkette ist keine Feldänderung.

lib/audit.ts liegt jetzt als lib/audit/write.ts neben lib/audit/query.ts.
2026-09-03 15:16:10 +02:00

204 lines
6.6 KiB
TypeScript

/**
* Rahmen der Anwendung: Name, Navigation, Anmeldung, Übersicht, Zustand.
*
* 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
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
*
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
* gezogen wird, verlässt den Server nicht.
*/
export const appText = {
name: 'AfiS-Aktensystem',
shortName: 'AfiS',
description: 'Fallakten, Personalakten und Ermittlungen der Abteilung für innere Sicherheit',
} as const
export const navText = {
dashboard: 'Übersicht',
cases: 'Fallakten',
personnel: 'Personalakten',
search: 'Suche',
admin: 'Admin',
skipToContent: 'Zum Inhalt springen',
} as const
export const authText = {
signIn: 'Mit Discord anmelden',
signOut: 'Abmelden',
signedInAs: 'Angemeldet als',
intro:
'Die Anmeldung läuft über Discord. Zugriff haben Mitglieder der Abteilung für innere Sicherheit.',
reasons: {
'keine-mitgliedschaft': {
title: 'Du bist nicht auf dem AfiS-Discord',
body: 'Die Anmeldung setzt eine Mitgliedschaft auf dem Discord-Server voraus. Wende dich an die Abteilungsleitung.',
},
'keine-afis-rolle': {
title: 'Keine Berechtigung für das Aktensystem',
body: 'Du bist auf dem Server, trägst aber keine Rolle der Abteilung. Wende dich an die Abteilungsleitung.',
},
'discord-nicht-erreichbar': {
title: 'Discord antwortet nicht',
body: 'Die Mitgliedschaft war nicht prüfbar. In einigen Minuten erneut versuchen.',
},
} as Record<string, { title: string; body: string }>,
sessionEnded: {
title: 'Sitzung beendet',
body: 'Deine Rollen haben sich geändert. Melde dich neu an.',
},
error: {
title: 'Anmeldung nicht möglich',
body: 'Der Anmeldevorgang wurde abgebrochen. Erneut versuchen.',
},
backToSignIn: 'Zurück zur Anmeldung',
} as const
export const dashboardText = {
heading: 'Übersicht',
mine: 'Meine Fälle',
mineEmpty: 'Du bist an keiner offenen Akte beteiligt.',
due: 'Fristen',
dueEmpty: 'Nichts steht an.',
dueAt: (datum: string) => `fällig ${datum}`,
overdue: 'überfällig',
unread: 'Ungelesen',
unreadEmpty: 'Nichts Neues.',
unreadCount: (n: number) => (n === 1 ? '1 Nachricht' : `${n} Nachrichten`),
watched: 'Beobachtet',
watchedEmpty: 'Keine Akte beobachtet.',
recent: 'Zuletzt geändert',
recentEmpty: 'Noch keine Akte.',
opened: 'Zuletzt geöffnet',
openedEmpty: 'Noch nichts geöffnet. Was du aufrufst, findest du hier wieder.',
openedHint: 'Nur für dich. Niemand sonst sieht diese Liste.',
openedClear: 'Liste leeren',
access: 'Dein Zugang',
} as const
export const watchText = {
watch: 'Beobachten',
unwatch: 'Nicht mehr beobachten',
hint: 'Beobachtete Akten stehen auf deiner Übersicht und melden sich, auch ohne Beteiligung.',
} as const
export const overviewText = {
heading: 'Dein Zugang',
intro: 'Was das System über dich weiss und was daraus folgt.',
person: 'Person',
discordId: 'Discord-Kennung',
roles: 'Rollen auf dem Discord',
clearance: 'Errechnete Freigabestufe',
permissions: 'Rechte',
noPermissions: 'Keine Rechte. Die Abteilungsleitung vergibt sie in der Rollenverwaltung.',
noRoles: 'Keine Rollen.',
bootstrapAdmin: 'Notfallzugang aus der Konfiguration, unabhängig vom Rollen-Mapping',
unmapped: 'nicht zugeordnet',
} as const
export const healthText = {
heading: 'Systemzustand',
service: 'Dienst',
state: 'Zustand',
checkedAt: (when: string) => `Zustand der Dienste, abgefragt ${when}`,
ok: 'Betriebsbereit',
degraded: 'Eingeschränkt',
down: 'Gestört',
components: {
database: 'Datenbank',
discordGateway: 'Discord-Gateway',
clamav: 'Virenprüfung',
} as Record<string, string>,
} as const
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.',
mode: 'Was soll dich erreichen?',
save: 'Übernehmen',
saved: 'Übernommen',
modes: {
ALL: 'Alles, was ich sehen darf',
OWN_CASES: 'Nur meine Fälle',
MENTIONS: 'Nur Erwähnungen',
OFF: 'Nichts',
} as Record<string, string>,
} as const
export const searchText = {
heading: 'Suche',
placeholder: 'Aktenzeichen, Titel, Inhalt',
kinds: {
case: 'Fallakten',
document: 'Dokumente',
personnel: 'Personalakten',
} as Record<string, string>,
filters: 'Filter',
reset: 'Filter zurücksetzen',
empty: 'Kein Treffer. Anderen Begriff oder weniger Filter versuchen.',
emptyNoQuery: 'Begriff eingeben oder einen Filter wählen.',
countLabel: (n: number) => (n === 1 ? '1 Treffer' : `${n} Treffer`),
more: 'Weitere laden',
truncated:
'Sehr viele Treffer. Angezeigt werden die besten; mit einem genaueren Begriff wird die Liste vollständig.',
views: 'Ansichten',
saveView: 'Ansicht speichern',
saveViewName: 'Name der Ansicht',
deleteView: 'Ansicht löschen',
builtin: {
mine: 'Meine Fälle',
dueThisWeek: 'Wiedervorlage diese Woche',
idle: 'Unbearbeitet seit 14 Tagen',
open: 'Offene Fälle',
sinceLastVisit: 'Seit deinem letzten Besuch geändert',
} as Record<string, string>,
} as const
export const paletteText = {
open: 'Springen',
placeholder: 'Aktenzeichen, Akte oder Seite',
hint: 'Strg+K',
empty: 'Nichts gefunden.',
close: 'Schliessen',
sections: {
cases: 'Fallakten',
personnel: 'Personalakten',
pages: 'Seiten',
recent: 'Zuletzt geöffnet',
} 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.
*/
pages: [
{ href: '/uebersicht', label: 'Übersicht' },
{ href: '/faelle', label: 'Fallakten' },
{ href: '/faelle/neu', label: 'Neue Fallakte' },
{ href: '/personalakten', label: 'Personalakten' },
{ href: '/suche', label: 'Suche' },
{ href: '/admin', label: 'Admin' },
{ href: '/admin/protokoll', label: 'Protokoll' },
{ href: '/einstellungen', label: 'Benachrichtigungen' },
],
} as const
export const profileText = {
open: 'Konto und Einstellungen',
signedInAs: 'Angemeldet als',
} as const
export const searchBarText = {
label: 'Suchen',
placeholder: 'Aktenzeichen, Titel, Name',
} as const
export const themeText = {
label: 'Thema',
system: 'Wie das System',
dark: 'Dunkel',
light: 'Hell',
} as const