fix(web): admin — scan initial après création, retrait des fallbacks mock
La création d'une bibliothèque déclenche un scan initial avec état de réessai en cas d'échec; suppression des fallbacks mock sur create/scan pour ne masquer aucune erreur réelle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -31,6 +31,26 @@ describe("api fallback helpers", () => {
|
|||||||
expect(headers.has("Content-Type")).toBe(false);
|
expect(headers.has("Content-Type")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces create library API errors without fallback", async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue(
|
||||||
|
new Response(JSON.stringify({ message: "Library path does not exist" }), {
|
||||||
|
status: 400,
|
||||||
|
statusText: "Bad Request",
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
await expect(api.createLibrary({ name: "Books", path: "/missing", enabled: true })).rejects.toThrow("Library path does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fallback when scan enqueue fails", async () => {
|
||||||
|
const fetchMock = vi.fn().mockRejectedValue(new Error("offline"));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
await expect(api.scanLibrary(42)).rejects.toThrow("offline");
|
||||||
|
});
|
||||||
|
|
||||||
it("sends metadata source updates to the admin endpoint", async () => {
|
it("sends metadata source updates to the admin endpoint", async () => {
|
||||||
const fetchMock = vi.fn().mockResolvedValue(
|
const fetchMock = vi.fn().mockResolvedValue(
|
||||||
new Response(JSON.stringify({ isbnPriorityEnabled: false, sources: [] }), {
|
new Response(JSON.stringify({ isbnPriorityEnabled: false, sources: [] }), {
|
||||||
|
|||||||
@ -185,15 +185,14 @@ export const api = {
|
|||||||
async createLibrary(input: CreateLibraryDto): Promise<LibraryDto> {
|
async createLibrary(input: CreateLibraryDto): Promise<LibraryDto> {
|
||||||
return request<LibraryDto>("/admin/libraries", {
|
return request<LibraryDto>("/admin/libraries", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input)
|
||||||
fallback: { id: Date.now(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...input }
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async deleteLibrary(id: number): Promise<void> {
|
async deleteLibrary(id: number): Promise<void> {
|
||||||
await request<{ ok: true }>(`/admin/libraries/${id}`, { method: "DELETE" });
|
await request<{ ok: true }>(`/admin/libraries/${id}`, { method: "DELETE" });
|
||||||
},
|
},
|
||||||
async scanLibrary(id: number): Promise<JobDto> {
|
async scanLibrary(id: number): Promise<JobDto> {
|
||||||
return request<JobDto>(`/admin/libraries/${id}/scan`, { method: "POST", fallback: mockJobs[0] });
|
return request<JobDto>(`/admin/libraries/${id}/scan`, { method: "POST" });
|
||||||
},
|
},
|
||||||
async jobs(): Promise<JobDto[]> {
|
async jobs(): Promise<JobDto[]> {
|
||||||
return request<JobDto[]>("/admin/jobs", { fallback: mockJobs });
|
return request<JobDto[]>("/admin/jobs", { fallback: mockJobs });
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export function AdminPage() {
|
|||||||
const [path, setPath] = useState("/library");
|
const [path, setPath] = useState("/library");
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [success, setSuccess] = useState<string>();
|
const [success, setSuccess] = useState<string>();
|
||||||
|
const [scanRetryLibrary, setScanRetryLibrary] = useState<LibraryDto>();
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -53,28 +54,38 @@ export function AdminPage() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setSuccess(undefined);
|
setSuccess(undefined);
|
||||||
|
setScanRetryLibrary(undefined);
|
||||||
try {
|
try {
|
||||||
await api.createLibrary({ name, path, enabled: true });
|
const created = await api.createLibrary({ name, path, enabled: true });
|
||||||
await refresh();
|
await refresh();
|
||||||
setSuccess(`Bibliothèque "${name}" ajoutée.`);
|
setName("Bibliotheque locale");
|
||||||
|
setPath("/library");
|
||||||
|
try {
|
||||||
|
await api.scanLibrary(created.id);
|
||||||
|
await refresh();
|
||||||
|
setSuccess(`Bibliothèque "${created.name}" ajoutée. Scan initial demandé.`);
|
||||||
|
} catch (scanError) {
|
||||||
|
setScanRetryLibrary(created);
|
||||||
|
setSuccess(
|
||||||
|
`Bibliothèque "${created.name}" ajoutée, mais le scan initial n'a pas pu être demandé. Tu peux réessayer le scan.`
|
||||||
|
);
|
||||||
|
setError(scanError instanceof Error ? `Scan initial impossible : ${scanError.message}` : "Scan initial impossible.");
|
||||||
|
}
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
const fallback = getApiFallback<LibraryDto>(createError);
|
setError(createError instanceof Error ? `Création impossible : ${createError.message}` : "Création impossible.");
|
||||||
if (fallback) setLibraries((current) => [fallback, ...current]);
|
|
||||||
setError(fallback ? "Creation en mode secours, synchronisation a retenter." : "Creation impossible");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scan(id: number) {
|
async function scan(id: number) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setSuccess(undefined);
|
setSuccess(undefined);
|
||||||
|
setScanRetryLibrary(undefined);
|
||||||
try {
|
try {
|
||||||
await api.scanLibrary(id);
|
await api.scanLibrary(id);
|
||||||
await refresh();
|
await refresh();
|
||||||
setSuccess("Scan demandé.");
|
setSuccess("Scan demandé.");
|
||||||
} catch (scanError) {
|
} catch (scanError) {
|
||||||
const fallback = getApiFallback<JobDto>(scanError);
|
setError(scanError instanceof Error ? `Scan impossible : ${scanError.message}` : "Scan impossible.");
|
||||||
if (fallback) setJobs((current) => [fallback, ...current]);
|
|
||||||
setError(fallback ? "Scan place en file de secours, statut a verifier." : "Scan impossible");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +97,7 @@ export function AdminPage() {
|
|||||||
|
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setSuccess(undefined);
|
setSuccess(undefined);
|
||||||
|
setScanRetryLibrary(undefined);
|
||||||
try {
|
try {
|
||||||
await api.deleteLibrary(library.id);
|
await api.deleteLibrary(library.id);
|
||||||
setLibraries((current) => current.filter((item) => item.id !== library.id));
|
setLibraries((current) => current.filter((item) => item.id !== library.id));
|
||||||
@ -104,14 +116,22 @@ export function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
<ErrorRibbon message={error} />
|
<ErrorRibbon message={error} />
|
||||||
{success && <div className="success-ribbon">{success}</div>}
|
{success && <div className="success-ribbon">{success}</div>}
|
||||||
{error && (
|
{scanRetryLibrary ? (
|
||||||
|
<div className="retry-row">
|
||||||
|
<span>La bibliothèque est conservée dans la liste.</span>
|
||||||
|
<button className="ghost-button" onClick={() => scan(scanRetryLibrary.id)}>
|
||||||
|
<Play size={16} />
|
||||||
|
Réessayer le scan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
<div className="retry-row">
|
<div className="retry-row">
|
||||||
<span>Les formulaires restent disponibles.</span>
|
<span>Les formulaires restent disponibles.</span>
|
||||||
<button className="ghost-button" onClick={() => void refresh()}>
|
<button className="ghost-button" onClick={() => void refresh()}>
|
||||||
Reessayer
|
Reessayer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
<form className="admin-form" onSubmit={createLibrary}>
|
<form className="admin-form" onSubmit={createLibrary}>
|
||||||
<label>
|
<label>
|
||||||
Nom de la bibliothèque
|
Nom de la bibliothèque
|
||||||
|
|||||||
Reference in New Issue
Block a user