feat: fehlende Upload-Dateien werden im Admin sofort gemeldet

Nach einem Redeploy waren alle hochgeladenen Bilder und Videos weg. Die
Ursache war bereits in a78c1cd behoben — das Volume "uploads" im
App-Deployment ist dort vom Platzhalter "emptyDir" auf einen
PersistentVolumeClaim umgestellt. Der Commit liegt auch auf origin/main.

Was fehlte, ist der Schritt danach: die Gitea-Pipeline baut nur das Image
und wendet KEINE Kubernetes-Manifeste an. Ein "kubectl rollout restart"
holt das neue Image, laesst die Deployment-Definition im Cluster aber
unveraendert — dort steht also weiterhin emptyDir. k8s/README.md bekommt
dafuer einen eigenen Abschnitt mit dem genauen Ablauf: erst pruefen, was
im Cluster tatsaechlich als Volume eingetragen ist, dann PVC und
Deployment anwenden, dann gegenpruefen.

Zusaetzlich das eigentliche Problem an diesem Ausfall: er ist unsichtbar.
Die Datenbank behaelt ihre Bildpfade, die Dateien dahinter fehlen, und im
Shop erscheinen einfach leere Flaechen — keine Fehlermeldung, kein
Log-Eintrag. Gemerkt hat man es erst, wenn jemand hingeschaut hat.

lib/upload-health.ts gleicht deshalb ab, ob die in Product/Harvest/Batch
und in den Inhaltsfeldern hinterlegten Pfade als Dateien vorliegen. Das
Admin-Dashboard zeigt bei Abweichung eine Warnung mit Anzahl und
Beispielpfaden samt Hinweis auf die Ursache. Der Abgleich laeuft bewusst
nach den Kennzahlen und mit eigenem catch: schlaegt er fehl, zeigt das
Dashboard trotzdem seine Zahlen. Obergrenze 300 gepruefte Pfade, damit die
Seite nicht an Dateisystemzugriffen haengt.

Bilder und Videos nutzen denselben UPLOAD_DIR (lib/uploads.ts), auch der
Video-Upload ueber den Umweg ueber ein Temp-Verzeichnis — es gibt also
keinen zweiten Speicherort, der getrennt behandelt werden muesste.

Gegen die lokale Datenbank geprueft: ohne Referenzen meldet der Abgleich
0/0, mit einem absichtlich auf eine nicht vorhandene Datei gesetzten
Produktbild 1 von 1 fehlend samt Pfad als Beleg, nach dem Zuruecksetzen
wieder 0/0.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-16 04:06:33 +02:00
1 parent bdce9b6a0a
commit c8ddcec04c
6 files changed
+182

No files matched your search

+29
View File
@@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
import { formatPrice } from "@/lib/format";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { checkUploadFiles } from "@/lib/upload-health";
const DASHBOARD_ICON_PROPS = {
width: 20,
@@ -224,9 +225,37 @@ export default async function AdminDashboardPage() {
const revenueTrend = [...revenueByDay.entries()];
const maxDailyRevenue = Math.max(1, ...revenueTrend.map(([, v]) => v));
// Bewusst NACH den Kennzahlen abgefragt und nicht in deren Promise.all:
// schlaegt der Dateiabgleich fehl (etwa weil das Upload-Verzeichnis gar
// nicht lesbar ist), soll das Dashboard trotzdem seine Zahlen zeigen.
const uploadHealth = await checkUploadFiles().catch(() => null);
return (
<div>
<h1 className="text-2xl text-forest mb-8">{t.adminDashboardPage.title}</h1>
{/* Fehlen Dateien, zu denen die Datenbank noch Pfade haelt, ist das
fast immer ein nicht persistentes Upload-Verzeichnis (in Kubernetes
ein "emptyDir" statt eines PersistentVolumeClaim). Ohne diesen
Hinweis faellt das erst auf, wenn Kundschaft im Shop leere
Produktkacheln sieht — im Log steht nichts. */}
{uploadHealth && uploadHealth.missing > 0 ? (
<div className="mb-8 border border-red-700/40 bg-red-50 p-4">
<p className="text-sm text-red-800">
{t.adminDashboardPage.missingUploadsTitle(uploadHealth.missing, uploadHealth.checked)}
</p>
<p className="mt-2 text-xs text-red-800/80">
{t.adminDashboardPage.missingUploadsHint}
</p>
{uploadHealth.examples.length > 0 ? (
<ul className="mt-2 text-xs text-red-800/70">
{uploadHealth.examples.map((example) => (
<li key={example}>{example}</li>
))}
</ul>
) : null}
</div>
) : null}
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{stats.map((stat) => {
const Icon = statIcons[stat.key];