feat: UI vue Review + backend sert le SPA, fin Phase 4 v1 (v0.24.0)
Vue Review (liste assets, approuver/refuser + confirm, refresh, 401→logout). Backend sert le SPA same-origin (@fastify/static + fallback) si CHLOVA_WEB_ROOT. Dockerfile multi-stage build web+API (contexte racine), image embarque /app/web. Compose contexte .., image chlova/backend:0.2.0. 65 tests, 0 vuln, compose OK. Palier de risque : privilégié (surface exposée complète) — non déployée ; auth + CHLOVA_PHASE requis pour activer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,97 @@
|
||||
// Vue Review — remplie en v0.24.0.
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useAuth } from "../auth";
|
||||
import { api, ApiError, type Asset } from "../api";
|
||||
|
||||
export function Review() {
|
||||
return <div className="p-6 text-muted">Review (v0.24.0)…</div>;
|
||||
const { token, logout } = useAuth();
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const guard = useCallback(
|
||||
(err: unknown): void => {
|
||||
if (err instanceof ApiError && err.status === 401) logout();
|
||||
else setError(err instanceof Error ? err.message : "Erreur");
|
||||
},
|
||||
[logout],
|
||||
);
|
||||
|
||||
const refresh = useCallback(async (): Promise<void> => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const { assets } = await api.review(token);
|
||||
setAssets(assets);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
guard(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token, guard]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const decide = async (id: string, action: "approve" | "refuse"): Promise<void> => {
|
||||
if (!token) return;
|
||||
if (action === "refuse" && !confirm(`Refuser définitivement ${id} ?`)) return;
|
||||
try {
|
||||
if (action === "approve") await api.approve(token, id);
|
||||
else await api.refuse(token, id);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
guard(err);
|
||||
}
|
||||
};
|
||||
|
||||
const badge = (a: Asset): string =>
|
||||
a.riskTier === "privileged" ? "text-danger" : "text-success";
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">Review</h2>
|
||||
<button onClick={() => void refresh()} className="ml-auto text-sm text-muted hover:text-fg cursor-pointer">
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <p role="alert" className="text-danger text-sm">{error}</p>}
|
||||
{loading && <p className="text-muted text-sm">Chargement…</p>}
|
||||
{!loading && assets.length === 0 && <p className="text-muted text-sm">Aucun asset en attente.</p>}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{assets.map((a) => (
|
||||
<li key={a.id} className="rounded-lg border border-border bg-surface p-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-mono text-sm">{a.id}</span>
|
||||
<span className={`text-xs ${badge(a)}`}>{a.riskTier}</span>
|
||||
<span className="text-xs text-muted">{a.status} · v{a.version}</span>
|
||||
{a.expiresAt && (
|
||||
<span className="text-xs text-warning">
|
||||
expire {new Date(a.expiresAt).toISOString().slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex gap-2">
|
||||
<button
|
||||
onClick={() => void decide(a.id, "approve")}
|
||||
className="rounded-md bg-success/20 text-success border border-success/40 px-3 py-1 text-sm cursor-pointer ring-accent"
|
||||
>
|
||||
Approuver
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void decide(a.id, "refuse")}
|
||||
className="rounded-md bg-danger/20 text-danger border border-danger/50 px-3 py-1 text-sm cursor-pointer ring-accent"
|
||||
>
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user