fix(web,api): débloquer la boucle de chargement 'Inventaire en cours' (Recherche/Admin)

- web: gestion d'erreur et d'état vide sur SearchPage/AdminPage
- web: client API — traitement des réponses sans contenu/erreurs réseau
- api: jobs.service — éviter le statut de scan perpétuellement en cours
- tests: client.test.ts

Refs: #12 (QA non-GREEN faute d'exécution, pas d'échec réel)
This commit is contained in:
Git Agent
2026-08-23 10:01:54 +02:00
parent 8f1140127f
commit e94524f119
6 changed files with 159 additions and 45 deletions

View File

@ -1,22 +1,37 @@
import { FormEvent, useEffect, useState } from "react";
import { Search } from "lucide-react";
import type { BookDto } from "@readabook/shared";
import { api } from "../api/client";
import { api, getApiFallback } from "../api/client";
import { BookCard } from "../components/BookCard";
import { EmptyState, LoadingState, Panel } from "../components/ui";
export function SearchPage() {
const [query, setQuery] = useState("");
const [books, setBooks] = useState<BookDto[] | null>(null);
const [books, setBooks] = useState<BookDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
async function loadBooks(nextQuery = query) {
setLoading(true);
setError(undefined);
try {
setBooks(nextQuery.trim() ? await api.search(nextQuery.trim()) : await api.books());
} catch (loadError) {
const fallback = getApiFallback<BookDto[]>(loadError);
setBooks(fallback ?? []);
setError(fallback ? "Catalogue indisponible, affichage de secours." : "Recherche indisponible.");
} finally {
setLoading(false);
}
}
useEffect(() => {
api.books().then(setBooks);
void loadBooks("");
}, []);
async function submit(event: FormEvent) {
event.preventDefault();
setBooks(null);
setBooks(query.trim() ? await api.search(query.trim()) : await api.books());
await loadBooks(query);
}
return (
@ -29,8 +44,16 @@ export function SearchPage() {
Chercher
</button>
</form>
{error && (
<div className="retry-row">
<span>{error}</span>
<button className="ghost-button" onClick={() => void loadBooks(query)}>
Reessayer
</button>
</div>
)}
</Panel>
{!books ? (
{loading && !books.length ? (
<LoadingState />
) : books.length ? (
<section className="book-grid span-3">
@ -40,7 +63,10 @@ export function SearchPage() {
</section>
) : (
<Panel className="span-3">
<EmptyState title="Aucun specimen" detail="Essaie un autre terme ou relance l'indexation." />
<EmptyState
title={loading ? "Recherche en cours" : "Aucun specimen"}
detail={loading ? "Le formulaire reste disponible." : "Essaie un autre terme ou relance l'indexation."}
/>
</Panel>
)}
</div>