feat(verzeichnis): LDAP-Abgleich, Zeitplan, CSV-Import und Log
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s

Quelle anlegen, Verbindung testen, nach Zeitplan oder auf Knopfdruck
abgleichen. Gelesen wird seitenweise mit searchPaginated, objectGUID
kommt als Puffer und wird gemischt-endian zur GUID umsortiert. Wer im
Verzeichnis verschwindet, wird hier inaktiv und nie geloescht.

Drei Regeln verhindern, dass eine kaputte Eingabe wie "alle sind
gegangen" aussieht: laesst sich kein einziger Eintrag zuordnen, wird
niemand deaktiviert; uebersprungene Eintraege werden am dn erkannt und
ausgenommen; eine von Hand angelegte Person wird uebernommen statt
verdoppelt.

Der Zeitplan laeuft ueber node-cron im selben Prozess, gestartet aus
src/instrumentation.ts, und nimmt keine eigene Sperre - die liegt in der
Datenbank, damit zwei Instanzen sich einig sind.

Closes #16

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-26 02:21:08 +02:00
co-authored by Claude Opus 5
parent ab5777a57e
commit 16059e7846
57 changed files with 6834 additions and 61 deletions
+69 -4
View File
@@ -18,7 +18,7 @@
* Bild sieht dann richtig aus, zeigt aber eine Seite, auf der kein Knopf etwas
* tut. Siehe CLAUDE.md, Abschnitt „Oberflaeche pruefen“.
*/
import { mkdir } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { chromium, type Browser, type Cookie, type Locator, type Page } from "@playwright/test";
@@ -191,6 +191,56 @@ const SEITEN: readonly Seite[] = [
},
},
{ name: "archiv", pfad: "/archiv?suche=Inventar" },
{ name: "verzeichnis", pfad: "/admin/verzeichnis", nurModus: "INTERN" },
{
/*
* Der Verbindungstest zeigt seinen Wert erst mit Ergebnis: die Tabelle der
* gemappten Felder. Also wird der Knopf gedrueckt und gewartet, bis der
* Server geantwortet hat — ohne Container steht dort eine Fehlermeldung,
* und auch die ist ein ehrliches Bild.
*/
name: "verbindungstest",
pfad: "/admin/verzeichnis",
nurModus: "INTERN",
vorbereiten: async (tab) => {
const knopf = tab.getByRole("button", { name: "Verbindung testen" }).first();
if ((await knopf.count()) === 0) return;
await knopf.click();
await tab.waitForTimeout(4000);
},
},
{
/*
* Das Abgleich-Log braucht einen Lauf, damit ueberhaupt etwas darin steht.
* Also erst „Jetzt abgleichen“ druecken und dann aufklappen — ob der Lauf
* gelingt, haengt am Testcontainer, und beide Ausgaenge sind ein ehrliches
* Bild: die Zeile zeigt Zahlen oder den Grund.
*/
name: "abgleich-log",
pfad: "/admin/verzeichnis",
nurModus: "INTERN",
vorbereiten: async (tab) => {
const knopf = tab.getByRole("button", { name: "Jetzt abgleichen" }).first();
if ((await knopf.count()) > 0) {
await knopf.click();
await tab.waitForTimeout(5000);
await tab.reload({ waitUntil: "load" });
}
const aufklappen = tab.locator("summary", { hasText: "Abgleich-Log" }).first();
if ((await aufklappen.count()) === 0) return;
await aufklappen.click();
await tab.waitForTimeout(150);
},
},
{
name: "personen-import",
pfad: "/mitarbeiter",
vorbereiten: async (tab) => {
await tab.locator("summary", { hasText: "CSV" }).first().click();
await tab.waitForTimeout(150);
},
},
{ name: "abteilungen-vorschlaege", pfad: "/admin/abteilungen" },
{
/*
* Die Druckansicht braucht einen Lauf, und welcher das ist, weiss nur die
@@ -263,15 +313,30 @@ async function anmelden(browser: Browser): Promise<Cookie[]> {
return kekse;
}
/** Erzeugt die PWA-Kacheln aus dem SVG, damit keine Bilddatei von Hand entsteht. */
/**
* Erzeugt die PWA-Kacheln aus dem SVG, damit keine Bilddatei von Hand entsteht.
*
* Geschrieben wird nur, wenn sich die Bytes wirklich unterscheiden. Vorher legte
* jeder Screenshot-Lauf zwei geaenderte PNG-Dateien in den Arbeitsbaum, obwohl das
* Bild dasselbe war — und `git status` zeigte Aenderungen, die niemand gemacht
* hatte (Reviewbefund zu #016).
*/
async function kachelnErzeugen(browser: Browser): Promise<void> {
const svg = new URL(`${APP_URL}/icon-kachel.svg`).toString();
for (const kante of [192, 512]) {
const seite = await browser.newPage({ viewport: { width: kante, height: kante } });
await seite.goto(svg, { waitUntil: "load" });
await seite.screenshot({ path: join("public", `icon-${kante}.png`), omitBackground: false });
const neu = await seite.screenshot({ omitBackground: false });
await seite.close();
console.log(` public/icon-${kante}.png`);
const datei = join("public", `icon-${kante}.png`);
const alt = await readFile(datei).catch(() => undefined);
if (alt && alt.equals(neu)) {
console.log(` ${datei} (unverändert)`);
continue;
}
await writeFile(datei, neu);
console.log(` ${datei}`);
}
}