fix: fehlende Dateien aus vorherigem Commit nachreichen
CSRF-Schutz, DHL-Versandmarken, Entra-ID-SSO, ApiCore-Sync und die Harvest-isPublished-Migration waren gestaged aber nicht im letzten Commit enthalten — Gitea-Actions-Build brach mit "Module not found" fuer genau diese Dateien ab. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
4d3100e9ba
commit
b6db9254a9
10 files changed
+1205
No files matched your search
@@ -0,0 +1,127 @@
|
||||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAdminAction } from "@/lib/audit-log";
|
||||
import {
|
||||
fetchApiCoreHarvests,
|
||||
ApiCoreConfigError,
|
||||
ApiCoreRequestError,
|
||||
type ApiCoreHarvestWithApiaryName,
|
||||
} from "@/lib/apicore-client";
|
||||
import { requireAdmin } from "./_shared";
|
||||
|
||||
/**
|
||||
* Stabiler, eindeutiger Slug aus der ApiCore-Ernte-ID statt aus Typ/Jahr
|
||||
* abgeleitet (wie beim manuellen createHarvestAction) — dient hier zugleich
|
||||
* als Idempotenz-Schluessel: ein wiederholter Sync erkennt an diesem Slug,
|
||||
* ob eine ApiCore-Ernte schon einmal importiert wurde, statt bei jedem Lauf
|
||||
* Duplikate anzulegen.
|
||||
*/
|
||||
function slugFor(sourceId: string): string {
|
||||
return `apicore-${sourceId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wabenhains Harvest.description ist ein Pflichtfeld, ApiCore liefert dafuer
|
||||
* kein Aequivalent — baut stattdessen eine sinnvolle Vorbefuellung aus den
|
||||
* vorhandenen Mengen-/Notizfeldern, die der Admin beim Review verfeinert.
|
||||
*/
|
||||
function buildDescription(source: ApiCoreHarvestWithApiaryName): string {
|
||||
const parts: string[] = [];
|
||||
if (source.honeySuperCount != null) parts.push(`${source.honeySuperCount} Honigräume`);
|
||||
if (source.grossWeight != null) parts.push(`Bruttogewicht ${source.grossWeight} kg`);
|
||||
if (source.netWeight != null) parts.push(`Nettogewicht ${source.netWeight} kg`);
|
||||
if (source.notes?.trim()) parts.push(source.notes.trim());
|
||||
const detail = parts.length > 0 ? parts.join(" · ") : "Bitte Beschreibung ergänzen.";
|
||||
return `Automatisch aus ApiCore importiert. ${detail}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zieht Ernten aus ApiCore und legt sie als unveroeffentlichte Entwuerfe an
|
||||
* (isPublished: false) bzw. aktualisiert bereits vorhandene, noch
|
||||
* unveroeffentlichte Entwuerfe. Bereits vom Admin veroeffentlichte,
|
||||
* frueher synchronisierte Ernten werden NICHT angefasst — sonst wuerde ein
|
||||
* erneuter Sync eine bewusste Freigabe- und Bearbeitungsentscheidung des
|
||||
* Admins stillschweigend zuruecksetzen.
|
||||
*
|
||||
* Admin-Berechtigungsfeld "type" bei Wabenhain (Erntesaison, z.B.
|
||||
* "Sommertracht") und ApiCores "honeyType" (Honigsorte, z.B. "Lindenhonig")
|
||||
* sind KEINE aequivalenten Taxonomien — es findet bewusst keine automatische
|
||||
* Uebersetzung statt, der Rohwert wird uebernommen und muss vom Admin beim
|
||||
* Review korrigiert werden.
|
||||
*/
|
||||
export async function syncHarvestsFromApiCoreAction(): Promise<void> {
|
||||
const admin = await requireAdmin();
|
||||
|
||||
let sourceHarvests: ApiCoreHarvestWithApiaryName[];
|
||||
try {
|
||||
sourceHarvests = await fetchApiCoreHarvests();
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof ApiCoreConfigError || error instanceof ApiCoreRequestError
|
||||
? error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "Unbekannter Fehler";
|
||||
redirect(`/admin/ernten?syncError=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const source of sourceHarvests) {
|
||||
const slug = slugFor(source.id);
|
||||
const date = new Date(source.date);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
failed++;
|
||||
console.error(`[apicore-sync] Ernte ${source.id} hat ein ungueltiges Datum ("${source.date}") — übersprungen.`);
|
||||
continue;
|
||||
}
|
||||
const type = source.honeyType?.trim() || `Ernte ${source.id.slice(0, 8)}`;
|
||||
|
||||
try {
|
||||
const existing = await prisma.harvest.findUnique({ where: { slug } });
|
||||
|
||||
if (existing?.isPublished) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = {
|
||||
year: date.getFullYear(),
|
||||
type,
|
||||
harvestDate: date,
|
||||
location: source.apiaryName ?? "Altenmünster",
|
||||
description: buildDescription(source),
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await prisma.harvest.update({ where: { slug }, data });
|
||||
updated++;
|
||||
} else {
|
||||
await prisma.harvest.create({ data: { ...data, slug, isPublished: false } });
|
||||
created++;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
failed++;
|
||||
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||
// z.B. P2002 (unique constraint year+type), falls zwei ApiCore-Ernten
|
||||
// im selben Jahr denselben honeyType tragen — bewusst nicht fatal fuer
|
||||
// den gesamten Sync-Lauf, siehe Kommentar oben zur Taxonomie-Kollision.
|
||||
console.error(`[apicore-sync] Ernte ${source.id} konnte nicht importiert werden: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await logAdminAction(admin, {
|
||||
action: "harvest.sync_apicore",
|
||||
entityType: "Harvest",
|
||||
detail: `${created} neu, ${updated} aktualisiert, ${skipped} übersprungen, ${failed} fehlgeschlagen`,
|
||||
});
|
||||
|
||||
revalidatePath("/admin/ernten");
|
||||
redirect(`/admin/ernten?synced=${created}&updated=${updated}&skipped=${skipped}&failed=${failed}`);
|
||||
}
|
||||
Reference in new issue
Block a user