Files
wijtransferen/frontend/src/pages/dashboard/DevPage.tsx
T
2026-08-22 17:27:55 +02:00

296 lines
9.7 KiB
TypeScript

import { useEffect, useState, type ReactNode } from 'react';
import { DashboardLayout } from '../../components/dashboard/DashboardLayout';
import { getDevDiagnostics, runDevStorageCheck } from '../../lib/api';
import type { DevDiagnostics, DevStorageCheck } from '../../types/dev';
interface UploadErrorLog {
at: string;
filename: string;
message: string;
}
export default function DevPage() {
const [diagnostics, setDiagnostics] = useState<DevDiagnostics | null>(null);
const [storageCheck, setStorageCheck] = useState<DevStorageCheck | null>(null);
const [uploadError, setUploadError] = useState<UploadErrorLog | null>(null);
const [loading, setLoading] = useState(true);
const [checking, setChecking] = useState(false);
const [error, setError] = useState('');
async function refresh() {
setLoading(true);
setError('');
try {
setDiagnostics(await getDevDiagnostics());
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Could not load diagnostics.');
} finally {
setLoading(false);
}
}
useEffect(() => {
void refresh();
try {
const raw = window.localStorage.getItem('studio:last-upload-error');
if (raw) setUploadError(JSON.parse(raw) as UploadErrorLog);
} catch {
setUploadError(null);
}
}, []);
async function checkStorage() {
setChecking(true);
try {
setStorageCheck(await runDevStorageCheck());
} catch (reason) {
setStorageCheck({
ok: false,
step: 'api',
error: reason instanceof Error ? reason.message : 'Storage check failed.',
});
} finally {
setChecking(false);
void refresh();
}
}
const browser = {
origin: window.location.origin,
online: navigator.onLine,
xhr: typeof XMLHttpRequest !== 'undefined',
fileApi: typeof File !== 'undefined' && typeof FileReader !== 'undefined',
secureContext: window.isSecureContext,
};
return (
<DashboardLayout>
<div className="studio-page dev-page">
<header className="studio-page__header studio-page__header--compact">
<div>
<p className="studio-kicker">Workspace / diagnostics</p>
<h1 className="studio-display">Dev Console</h1>
<p className="studio-page__lede">Local runtime diagnostics and storage checks.</p>
</div>
<div className="dev-page__actions">
<button
className="editor-button editor-button--quiet"
type="button"
onClick={() => void refresh()}
disabled={loading}
>
Refresh
</button>
<button
className="editor-button editor-button--accent"
type="button"
onClick={() => void checkStorage()}
disabled={checking}
>
{checking ? 'Checking...' : 'Test storage'}
</button>
</div>
</header>
{error && <p className="studio-alert studio-alert--error">{error}</p>}
<div className="dev-grid">
<DevPanel title="Runtime" eyebrow="01 / API">
{loading || !diagnostics ? (
<DevLoading />
) : (
<>
<DevRow label="Environment" value={diagnostics.environment} />
<DevRow label="API time" value={diagnostics.now} />
<DevRow label="Account" value={diagnostics.user.email} />
<DevRow
label="Session cookie"
value={diagnostics.http.cookieSecure ? 'Secure' : 'Local / insecure'}
/>
</>
)}
</DevPanel>
<DevPanel title="Database" eyebrow="02 / Persistence">
{loading || !diagnostics ? (
<DevLoading />
) : (
<>
<DevStatus
label={diagnostics.database.driver}
ok={diagnostics.database.connected}
/>
<DevRow
label="Connection"
value={
diagnostics.database.connected
? 'Reachable'
: diagnostics.database.error || 'Unavailable'
}
/>
</>
)}
</DevPanel>
<DevPanel title="Object storage" eyebrow="03 / MinIO">
{loading || !diagnostics ? (
<DevLoading />
) : (
<>
<DevStatus
label={diagnostics.storage.provider}
ok={diagnostics.storage.reachable}
/>
<DevRow label="Endpoint" value={diagnostics.storage.endpoint} />
<DevRow label="Bucket" value={diagnostics.storage.bucket} />
<DevRow label="Protocol" value={diagnostics.storage.secure ? 'HTTPS' : 'HTTP'} />
{diagnostics.storage.error && (
<p className="dev-panel__error">{diagnostics.storage.error}</p>
)}
</>
)}
</DevPanel>
<DevPanel title="Browser" eyebrow="04 / Client">
<DevRow label="Origin" value={browser.origin} />
<DevRow label="Network" value={browser.online ? 'Online' : 'Offline'} />
<DevRow label="XMLHttpRequest" value={browser.xhr ? 'Available' : 'Unavailable'} />
<DevRow label="File API" value={browser.fileApi ? 'Available' : 'Unavailable'} />
<DevRow
label="Secure context"
value={browser.secureContext ? 'Yes' : 'No (normal for local HTTP)'}
/>
</DevPanel>
</div>
<section className="dev-panel dev-panel--wide">
<div className="dev-panel__heading">
<p className="studio-kicker">05 / Upload path</p>
<h2>What happens when you choose a file.</h2>
</div>
<div className="dev-flow">
<span>
01 <strong>API URL</strong>
<small>Creates media metadata</small>
</span>
<i>&#8594;</i>
<span>
02 <strong>Presigned PUT</strong>
<small>Browser to MinIO</small>
</span>
<i>&#8594;</i>
<span>
03 <strong>Complete</strong>
<small>Stat object and queue processing</small>
</span>
<i>&#8594;</i>
<span>
04 <strong>Ready</strong>
<small>Preview becomes visible</small>
</span>
</div>
<p className="dev-hint">
If an upload stops at 0%, the failure is usually the browser reaching the presigned
MinIO origin. Check that the browser origin below is listed in the MinIO bucket CORS
rule.
</p>
{diagnostics && (
<div className="dev-cors">
<span>Effective browser origins</span>
{diagnostics.http.corsOrigins.map((origin) => (
<code key={origin}>{origin}</code>
))}
</div>
)}
</section>
<section className="dev-panel dev-panel--wide">
<div className="dev-panel__heading">
<p className="studio-kicker">06 / Last client failure</p>
<h2>Recent upload signal.</h2>
</div>
{uploadError ? (
<div className="dev-last-error">
<span className="dev-last-error__mark">!</span>
<div>
<strong>{uploadError.filename}</strong>
<p>{uploadError.message}</p>
<small>{new Date(uploadError.at).toLocaleString()}</small>
</div>
<button
type="button"
onClick={() => {
window.localStorage.removeItem('studio:last-upload-error');
setUploadError(null);
}}
>
Clear
</button>
</div>
) : (
<p className="dev-empty">
No client-side upload failures have been recorded in this browser.
</p>
)}
{storageCheck && (
<div className={`dev-check-result ${storageCheck.ok ? 'is-ok' : 'is-failed'}`}>
<strong>{storageCheck.ok ? 'Storage check passed' : 'Storage check failed'}</strong>
<span>
{storageCheck.ok
? `${storageCheck.bytes} bytes / ${storageCheck.contentType}`
: `${storageCheck.step}: ${storageCheck.error}`}
</span>
</div>
)}
</section>
<details className="dev-raw">
<summary>Show raw diagnostics JSON</summary>
<pre>{diagnostics ? JSON.stringify(diagnostics, null, 2) : 'No response yet.'}</pre>
</details>
</div>
</DashboardLayout>
);
}
function DevPanel({
title,
eyebrow,
children,
}: {
title: string;
eyebrow: string;
children: ReactNode;
}) {
return (
<section className="dev-panel">
<div className="dev-panel__heading">
<p className="studio-kicker">{eyebrow}</p>
<h2>{title}</h2>
</div>
{children}
</section>
);
}
function DevRow({ label, value }: { label: string; value: string }) {
return (
<div className="dev-row">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
function DevStatus({ label, ok }: { label: string; ok: boolean }) {
return (
<div className={`dev-status ${ok ? 'is-ok' : 'is-failed'}`}>
<i /> <strong>{label}</strong>
<span>{ok ? 'reachable' : 'unavailable'}</span>
</div>
);
}
function DevLoading() {
return (
<div className="dev-loading">
<span />
<span />
<span />
</div>
);
}