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:
1 parent
bdce9b6a0a
commit
c8ddcec04c
6 files changed
+182
No files matched your search
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user