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
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
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
* 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.
*/
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
* 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.
*/
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
* 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.
*/
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.
* 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.
*/
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
* 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 }) {
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.
* 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.
*/
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.
*
* 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.
*/
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.
* 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.
*/
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
* 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> {
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.
* 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.
*/
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.
* 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.
*/
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
* 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
* die höchste. Damit steht die Hierarchie an einer Stelle, die ohnehin gepflegt
* 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.
*
* 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
* 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.
* 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.
*/
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
* Mittelwert — ein Ausreisser auf einem alten Laptop soll die Zahl nicht
* Mittelwert - ein Ausreisser auf einem alten Laptop soll die Zahl nicht
* bestimmen.
*/
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.
* 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.
*/
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.
* 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> {
const user = await requireUser()
@@ -14,7 +14,7 @@ import {
* Ausfertigen, Zurückziehen, Umklassifizieren.
*
* 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.
*/
export function DocumentActions({
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
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']) })
/** 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(
_prev: ActionResult | null,
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.
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -28,7 +28,7 @@ const requestSchema = z.union([
/**
* 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
* auf Playwright wartet. Siehe docs/09-performance-und-ux.md.
*/
@@ -90,7 +90,7 @@ export function CaseActions({
</button>
</div>
{/* 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. */}
<p className="mt-1.5 max-w-sm text-2xs text-text-dim">
{caseText.statusHints[nextStatus] ?? caseText.statusChangeHint}
@@ -113,7 +113,7 @@ export function Participants({
))}
</PanelSelect>
<PanelSelect value={target} onChange={setTarget} label={participantText.person}>
<option value="">—</option>
<option value="">-</option>
<optgroup label={participantText.pickUser}>
{members.map((member) => (
<option key={member.id} value={`user:${member.id}`}>
@@ -7,7 +7,7 @@ import { toggleWatchAction } from '../actions'
/**
* 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
* 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.
* 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.
*/
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,
* 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({
params,
@@ -2,7 +2,7 @@ import { Skeleton, SkeletonLine } from '@/components/Skeleton'
/**
* 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.
*/
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.
* 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.
*/
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.
* 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.
*/
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
* umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte
* 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
* gleichzeitig braucht.
*
* 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.
*/
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
* 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.
*/
export default async function CasePage({
@@ -51,7 +51,7 @@ export async function saveSectionAction(
/**
* 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
* übrigen nicht aufhalten. Wäre das eine Transaktion, blockierte ein einziger
* fremder Eingriff das ganze Blatt.
+2 -2
View File
@@ -66,7 +66,7 @@ export async function createCaseAction(
caseNumber = created.caseNumber
// 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.
for (const eintrag of beteiligte) {
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 }
/** 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> {
const user = await requireUser()
const parsed = watchSchema.safeParse(input)
+2 -2
View File
@@ -25,7 +25,7 @@ export function NewCaseForm({
const [state, action, pending] = useActionState<CreateState, FormData>(createCaseAction, {})
// 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.
const [beteiligte, setBeteiligte] = useState<Entwurf[]>([])
const [naechster, setNaechster] = useState(1)
@@ -133,7 +133,7 @@ export function NewCaseForm({
onChange={(event) => aendern(eintrag.schluessel, { ziel: event.target.value })}
className="field grow"
>
<option value="">—</option>
<option value="">-</option>
<optgroup label={participantText.pickUser}>
{members.map((member) => (
<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.
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -82,7 +82,7 @@ export default async function CaseListPage({
</Link>
{/* Alle sieben Zustände, nicht fünf: „Entwurf" und „Versiegelt" fehlten,
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. */}
{CASE_STATUSES.filter(
(value) =>
+2 -2
View File
@@ -13,8 +13,8 @@ import { hasAdminAccess } from './admin/sections'
/**
* Das Rahmenwerk der angemeldeten Anwendung.
*
* Die Kopfzeile trägt links die Bereiche der Arbeit — Übersicht, Fallakten,
* Personalakten, Verwaltung — und rechts alles, was den Nutzer selbst
* Die Kopfzeile trägt links die Bereiche der Arbeit - Übersicht, Fallakten,
* Personalakten, Verwaltung - und rechts alles, was den Nutzer selbst
* betrifft: Suchbalken und Profilbild. Thema, Benachrichtigungen und Abmelden
* standen früher zwischen den Aktenbereichen und nahmen dort den Platz weg,
* 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.
*
* 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({
personnelFileId,
@@ -94,7 +94,7 @@ export function Assignments({
))}
</PanelSelect>
<PanelSelect value={userId} onChange={setUserId} label={participantText.pickUser}>
<option value="">—</option>
<option value="">-</option>
{members.map((member) => (
<option key={member.id} value={member.id}>
{member.displayName}
@@ -12,7 +12,7 @@ import { linkCaseToPersonAction } from '../actions'
*
* Derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von der
* 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.
*/
export function LinkCase({ personnelFileId }: { personnelFileId: string }) {
@@ -27,7 +27,7 @@ type File = {
* Kopfdaten einer Personalakte: lesen und ändern in derselben Ansicht.
*
* 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.
*/
export function PersonnelHead({
@@ -276,7 +276,7 @@ function Zeile({
return (
<div className="flex gap-3">
<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>
)
}
+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
* 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.
*/
async function ChatBlock({
+1 -1
View File
@@ -71,7 +71,7 @@ export async function createPersonnelAction(
id = created.id
// 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.
await erstesAktenblatt(user.permissions, id, parsed.data.displayName)
} catch (error) {
@@ -8,8 +8,8 @@ import { createPersonnelAction, type ActionResult } from '../actions'
/**
* Neue Personalakte.
*
* Das Discord-Konto ist **optional**. Personen ohne Konto — Zeugen, ehemalige
* Mitarbeiter, Externe — brauchen genauso eine Akte, und ihr Name ist dann das
* Das Discord-Konto ist **optional**. Personen ohne Konto - Zeugen, ehemalige
* Mitarbeiter, Externe - brauchen genauso eine Akte, und ihr Name ist dann das
* einzige, was sie identifiziert.
*/
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.
* 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.
*/
export default function Loading() {
+1 -1
View File
@@ -22,7 +22,7 @@ export default async function PersonnelListPage({
}: {
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 { q, status } = await searchParams
+1 -1
View File
@@ -9,7 +9,7 @@ import { deleteViewAction, saveViewAction } from './actions'
/**
* 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
* in der Datenbank.
*/
+1 -1
View File
@@ -10,7 +10,7 @@ const DEBOUNCE_MS = 250
/**
* 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
* Seite lädt, bleiben die alten Treffer stehen und werden gedimmt, statt zu
* 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.
* 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.
*/
export default function Loading() {
+5 -5
View File
@@ -30,7 +30,7 @@ export const dynamic = 'force-dynamic'
* Suchseite.
*
* 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.
*/
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,
// 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.
const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)!
const gesamt = alle
@@ -108,7 +108,7 @@ export default async function SearchPage({
{SEARCH_SCOPES.map((kind) => {
const anzahl = kind === 'all' ? gesamt : zahlen[kind]
// `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.
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
* 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.
*/
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.
*/
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.
* 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.
*/
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
* 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.
*/
export default async function DashboardPage() {
@@ -206,13 +206,13 @@ function Block({
/**
* 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.
*
* 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
* 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
* 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.
* 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.
*/
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
* 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
* 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 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,
* also darf dort keine Kopfzeile stehen, die `requireUser()` braucht. Genau
* 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`
* 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
* Zurück-Knopf nichts verloren.
@@ -23,7 +23,7 @@ export async function generateMetadata({
/**
* 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,
* ein Vertrag als Vertrag. Siehe docs/08-suche-und-export.md.
*/
@@ -169,7 +169,7 @@ export default async function PrintDocumentPage({
</p>
) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')}
{String(section.value ?? '-')}
</p>
)}
</div>
@@ -99,7 +99,7 @@ export default async function PrintCasePage({
/>
<Zeile
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
@@ -164,7 +164,7 @@ export default async function PrintCasePage({
<td>{anhang.originalName}</td>
<td>{anhang.sizeBytes}</td>
{/* 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' }}>
{anhang.sha256}
</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
* 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.
*/
function PrintSections({
@@ -256,7 +256,7 @@ function PrintSections({
</p>
) : (
<p style={{ margin: '1mm 0', whiteSpace: 'pre-wrap' }}>
{String(section.value ?? '—')}
{String(section.value ?? '-')}
</p>
)}
</div>
+2 -2
View File
@@ -10,9 +10,9 @@ export const dynamic = 'force-dynamic'
* kurzlebigen Zeichen und ohne Sitzung, und eine Weiterleitung zur Anmeldung
* 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
* 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.
*/
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
*
* 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
* Akte.
*/
+1 -1
View File
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
* Herunterladen eines fertigen Exports.
*
* 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.
*/
export async function GET(
+1 -1
View File
@@ -13,7 +13,7 @@ export const runtime = 'nodejs'
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.
* 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.
*
* 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.
*/
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.
*
* 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`.
*
* 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
// 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') {
const zuletzt = await listRecent(user.permissions, RECENT_IN_PALETTE)
return NextResponse.json({
+3 -3
View File
@@ -30,7 +30,7 @@ const HEARTBEAT_MS = 25_000
*
* Drei Prüfungen, alle nötig:
* 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.
* 2. **Je Ereignis**: der Inhalt wird mit den Rechten des Empfängers geladen.
* 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
* Verbinden eingefroren. Eine offene Verbindung ist sonst der eine Ort im
* 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.
*/
export async function GET(request: Request): Promise<Response> {
@@ -161,7 +161,7 @@ export async function GET(request: Request): Promise<Response> {
return
}
// 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
// die **einzige** Schranke.
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.
*
* 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.
*/
export async function POST(request: Request): Promise<Response> {
+7 -7
View File
@@ -56,7 +56,7 @@
/* Struktur: Flaechen bleiben eckig, Bedienelemente sind rund.
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-sm: 2px;
--radius-DEFAULT: 2px;
@@ -87,7 +87,7 @@
* Klassifizierungsfarben: eine Stufe muss in beiden Themen dieselbe Farbe
* 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
* mindestens 4,5:1 für Fliesstext ausgelegt. */
[data-theme='light'] {
@@ -226,7 +226,7 @@
**Und er muss beim Druecken zurueckmelden, dass er getroffen wurde.**
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
setzt ohne Verzoegerung ein und klingt langsam ab: ein kurzer Klick
bliebe sonst unsichtbar. Siehe docs/10-design-system.md.
@@ -318,7 +318,7 @@
Jede Liste in dieser Anwendung zeigt dasselbe Gebilde: eine Kennung, ein
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"
untereinander sieht man das sofort.
@@ -350,7 +350,7 @@
}
/* 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. */
.record-label {
flex: none;
@@ -364,7 +364,7 @@
}
/* 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. */
.record-rank {
flex: none;
@@ -827,7 +827,7 @@
/* Der Grund unter einer Druckansicht.
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
weg — Papier bringt seinen eigenen Grund mit. */
weg - Papier bringt seinen eigenen Grund mit. */
.print-page {
min-height: 100dvh;
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
* 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 =
`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.
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
nachladen kann. React vergleicht beim Hydrieren aber genau dieses
Attribut, liest clientseitig den leeren Wert und meldet einen