Merge branch 'phase/027-protokoll-ansicht'
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { aktionenDerGruppe } from "@/lib/protokoll-aktionen";
|
||||
import { zeitraum, type ProtokollFilter } from "@/lib/protokoll-filter";
|
||||
|
||||
/**
|
||||
* Lesemodell des Protokolls (#027). Jede Rolle darf das ganze Protokoll lesen (PLAN §4);
|
||||
* die Rechtepruefung machen Seite, Action und Route selbst.
|
||||
*
|
||||
* Geblaettert wird ueber die ID („älter als“), nicht ueber OFFSET: das Protokoll waechst
|
||||
* waehrend man liest, und OFFSET wuerde dann Zeilen doppelt zeigen oder ueberspringen –
|
||||
* ausserdem muesste die Datenbank bei OFFSET 10 000 erst 10 000 Zeilen lesen und verwerfen.
|
||||
*/
|
||||
export const SEITENGROESSE = 50;
|
||||
|
||||
export type ProtokollZeile = {
|
||||
readonly id: string;
|
||||
readonly zeitpunkt: string;
|
||||
readonly akteurName: string;
|
||||
readonly aktion: string;
|
||||
readonly ergebnis: "ERFOLG" | "FEHLER" | "VERWEIGERT";
|
||||
readonly hostName: string | null;
|
||||
readonly zielbenutzer: string | null;
|
||||
readonly fingerprint: string | null;
|
||||
readonly diff: string | null;
|
||||
/** Bereits beim Schreiben geschwaerzt (#005); oberste Ebene als Liste, Werte formatiert. */
|
||||
readonly details: { readonly name: string; readonly wert: string }[] | null;
|
||||
readonly vorgaengerHash: string;
|
||||
readonly hash: string;
|
||||
};
|
||||
|
||||
const AUSWAHL = {
|
||||
id: true,
|
||||
zeitpunkt: true,
|
||||
akteurName: true,
|
||||
aktion: true,
|
||||
ergebnis: true,
|
||||
hostName: true,
|
||||
zielbenutzer: true,
|
||||
fingerprint: true,
|
||||
diff: true,
|
||||
details: true,
|
||||
vorgaengerHash: true,
|
||||
hash: true,
|
||||
} satisfies Prisma.ProtokollSelect;
|
||||
|
||||
type Rohzeile = Prisma.ProtokollGetPayload<{ select: typeof AUSWAHL }>;
|
||||
|
||||
export function protokollWhere(filter: ProtokollFilter, vor?: bigint): Prisma.ProtokollWhereInput {
|
||||
const und: Prisma.ProtokollWhereInput[] = [];
|
||||
const { ab, vor: bis } = zeitraum(filter);
|
||||
if (ab || bis) und.push({ zeitpunkt: { ...(ab ? { gte: ab } : {}), ...(bis ? { lt: bis } : {}) } });
|
||||
if (filter.gruppe) und.push({ aktion: { in: aktionenDerGruppe(filter.gruppe) } });
|
||||
if (filter.akteur) und.push({ akteurName: filter.akteur });
|
||||
if (filter.host) und.push({ hostName: { contains: filter.host, mode: "insensitive" } });
|
||||
if (filter.ergebnis) und.push({ ergebnis: filter.ergebnis });
|
||||
if (filter.q) {
|
||||
const enthaelt = { contains: filter.q, mode: "insensitive" } as const;
|
||||
und.push({ OR: [{ fingerprint: enthaelt }, { hostName: enthaelt }, { zielbenutzer: enthaelt }] });
|
||||
}
|
||||
if (vor !== undefined) und.push({ id: { lt: vor } });
|
||||
return und.length ? { AND: und } : {};
|
||||
}
|
||||
|
||||
/** Texte bleiben Texte; Zahlen, Listen und Objekte als eingerueckte JSON. */
|
||||
function detailsListe(details: Prisma.JsonValue): ProtokollZeile["details"] {
|
||||
if (details == null) return null;
|
||||
const alsText = (wert: unknown) => (typeof wert === "string" ? wert : JSON.stringify(wert, null, 2));
|
||||
if (typeof details !== "object" || Array.isArray(details))
|
||||
return [{ name: "Wert", wert: alsText(details) }];
|
||||
const eintraege = Object.entries(details).map(([name, wert]) => ({ name, wert: alsText(wert) }));
|
||||
return eintraege.length ? eintraege : null;
|
||||
}
|
||||
|
||||
function zeileAus(z: Rohzeile): ProtokollZeile {
|
||||
return {
|
||||
id: String(z.id),
|
||||
zeitpunkt: z.zeitpunkt.toISOString(),
|
||||
akteurName: z.akteurName,
|
||||
aktion: z.aktion,
|
||||
ergebnis: z.ergebnis,
|
||||
hostName: z.hostName,
|
||||
zielbenutzer: z.zielbenutzer,
|
||||
fingerprint: z.fingerprint,
|
||||
diff: z.diff,
|
||||
details: detailsListe(z.details),
|
||||
vorgaengerHash: z.vorgaengerHash,
|
||||
hash: z.hash,
|
||||
};
|
||||
}
|
||||
|
||||
export type ProtokollSeite = {
|
||||
readonly zeilen: ProtokollZeile[];
|
||||
/** ID fuer „ältere laden“; fehlt, wenn es nichts Aelteres mehr gibt. */
|
||||
readonly weiterVor: string | null;
|
||||
};
|
||||
|
||||
/** Eine Seite, neueste zuerst; `vor` ist die kleinste ID der vorigen Seite. */
|
||||
export async function protokollSeite(filter: ProtokollFilter, vor?: bigint): Promise<ProtokollSeite> {
|
||||
const roh = await prisma.protokoll.findMany({
|
||||
where: protokollWhere(filter, vor),
|
||||
orderBy: { id: "desc" },
|
||||
take: SEITENGROESSE + 1,
|
||||
select: AUSWAHL,
|
||||
});
|
||||
const mehr = roh.length > SEITENGROESSE;
|
||||
const zeilen = roh.slice(0, SEITENGROESSE).map(zeileAus);
|
||||
return { zeilen, weiterVor: mehr ? (zeilen.at(-1)?.id ?? null) : null };
|
||||
}
|
||||
|
||||
/** Kopfzeile der Seite: Gesamtzahl und Treffer des Filters. */
|
||||
export async function protokollZaehlen(
|
||||
filter: ProtokollFilter,
|
||||
): Promise<{ gesamt: number; treffer: number }> {
|
||||
const [gesamt, treffer] = await Promise.all([
|
||||
prisma.protokoll.count(),
|
||||
prisma.protokoll.count({ where: protokollWhere(filter) }),
|
||||
]);
|
||||
return { gesamt, treffer };
|
||||
}
|
||||
|
||||
/** Namen fuer den Akteur-Filter – haeufigste zuerst, begrenzt, damit die Liste handlich bleibt. */
|
||||
export async function protokollAkteure(): Promise<string[]> {
|
||||
const gruppen = await prisma.protokoll.groupBy({
|
||||
by: ["akteurName"],
|
||||
_count: { _all: true },
|
||||
orderBy: { _count: { akteurName: "desc" } },
|
||||
take: 100,
|
||||
});
|
||||
return gruppen.map((g) => g.akteurName).sort((a, b) => a.localeCompare(b, "de"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Treffer in Stapeln, neueste zuerst – fuer den CSV-Export. Liest je Stapel nur
|
||||
* `stapel` Zeilen, damit auch ein grosses Protokoll nie ganz im Speicher liegt.
|
||||
*/
|
||||
export async function* protokollStapelweise(
|
||||
filter: ProtokollFilter,
|
||||
stapel = 500,
|
||||
): AsyncGenerator<Rohzeile[], void, undefined> {
|
||||
let vor: bigint | undefined;
|
||||
for (;;) {
|
||||
const zeilen = await prisma.protokoll.findMany({
|
||||
where: protokollWhere(filter, vor),
|
||||
orderBy: { id: "desc" },
|
||||
take: stapel,
|
||||
select: AUSWAHL,
|
||||
});
|
||||
if (zeilen.length === 0) return;
|
||||
yield zeilen;
|
||||
if (zeilen.length < stapel) return;
|
||||
vor = zeilen.at(-1)!.id;
|
||||
}
|
||||
}
|
||||
|
||||
export type { Rohzeile as ProtokollRohzeile };
|
||||
Reference in New Issue
Block a user