feat(laeufe): Service, Abarbeitung, Kommentare und Archivschutz
Ein Lauf wird vorbereitet, abgearbeitet, abgeschlossen und archiviert. Der Archiv-Schreibschutz sitzt in genau einer Funktion und wird vor jeder Aenderung an Kopfdaten, Schrittergebnis und Kommentar gerufen; abgeschlossen bleibt aenderbar, archiviert nicht. Die Abarbeitungsansicht speichert jeden Schritt einzeln, frischt sich alle zehn Sekunden auf und pausiert im Hintergrund. Sie traegt in der Polling-Antwort keine Befehle und laedt sich auch nicht von selbst neu, solange ein generiertes Passwort auf der Seite steht: beides tauschte es dem Techniker zwischen Lesen und Einfuegen aus. Der unabhaengige Review fand zwei Blocker: ein Befehl konnte zwei verschiedene Passwoerter tragen (Konto bekommt A, Benutzer erfaehrt B), und „nicht zutreffend“ liess sich nicht zuruecknehmen, waehrend die Oberflaeche behauptete, nichts sei gewaehlt. Beides behoben und in Tests festgehalten. Closes #13 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #27.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { laufZustand } from "@/server/queries/laeufe";
|
||||
|
||||
/**
|
||||
* Der Zustand eines Laufs fuer die Aktualisierung alle zehn Sekunden (#013).
|
||||
*
|
||||
* **Route Handler pruefen selbst.** Die Huelle unter `src/app/(app)/layout.tsx`
|
||||
* deckt sie nicht ab — sie liegt in einem anderen Zweig des Baums, und der Proxy
|
||||
* sieht nur, **ob** ein Cookie da ist, nie was drinsteht (CLAUDE.md).
|
||||
*
|
||||
* Die Antwort enthaelt **keine Befehle**. Ein `{{generiertes_passwort}}`
|
||||
* entsteht bei jeder Aufloesung neu; alle zehn Sekunden mitgeliefert, tauschte
|
||||
* es dem Techniker das Passwort unter den Haenden weg, waehrend er es abtippt.
|
||||
* Zurueck kommen nur Status und Schrittzustaende — genau das, was zwei Browser
|
||||
* am selben Lauf voneinander wissen muessen.
|
||||
*/
|
||||
export async function GET(
|
||||
_anfrage: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
): Promise<Response> {
|
||||
let benutzerId: string;
|
||||
let rolle: Parameters<typeof laufZustand>[1]["rolle"];
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("lesen");
|
||||
benutzerId = benutzer.id;
|
||||
rolle = benutzer.rolle;
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RechteFehler) {
|
||||
// 401 und nicht 403: der Client soll die Aktualisierung einstellen und
|
||||
// den Benutzer zur Anmeldung schicken, statt weiter zu fragen.
|
||||
return Response.json({ fehler: "Nicht angemeldet." }, { status: 401 });
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const zustand = await laufZustand(id, { id: benutzerId, rolle });
|
||||
if (!zustand) {
|
||||
// Nicht vorhanden und nicht sichtbar sind dieselbe Antwort – die
|
||||
// Unterscheidung waere schon eine Auskunft ueber fremde Kunden.
|
||||
return Response.json({ fehler: "Diesen Lauf gibt es nicht." }, { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json(zustand, {
|
||||
// Nichts zwischenspeichern: die Antwort ist genau dann wertlos, wenn sie
|
||||
// aus einem Zwischenspeicher kommt.
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user