perf(api,indexer): optimiser pages, thumbnails, watcher et robustesse fd

- Pages: mode Original (zero-transcoding), ETag/304, cache index CBZ,
  préfetch next 2 pages, filtre Triangle par défaut
- Thumbnails: DCT scaling JPEG via jpeg-decoder (decode 7x plus rapide),
  img.thumbnail() pour resize, support format Original, fix JPEG RGBA8
- API fallback thumbnail: OutputFormat::Original + DCT scaling au lieu
  de WebP full-decode, retour (bytes, content_type) dynamique
- Watcher: remplacement notify par poll léger sans inotify/fd,
  skip poll quand job actif, snapshots en mémoire
- Jobs: mutex exclusif corrigé (tous statuts actifs, tous types exclusifs)
- Robustesse: suppression fs::canonicalize (problèmes fd Docker),
  list_folders avec erreurs explicites, has_children default true
- Backoffice: FormRow items-start pour alignement inputs avec helper text,
  labels settings clarifiés

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:07:42 +01:00
parent fe54f55f47
commit 6947af10fe
15 changed files with 711 additions and 395 deletions

View File

@@ -584,6 +584,17 @@ use axum::{
response::IntoResponse,
};
/// Detect content type from thumbnail file extension.
fn detect_thumbnail_content_type(path: &str) -> &'static str {
if path.ends_with(".jpg") || path.ends_with(".jpeg") {
"image/jpeg"
} else if path.ends_with(".png") {
"image/png"
} else {
"image/webp"
}
}
/// Get book thumbnail image
#[utoipa::path(
get,
@@ -612,9 +623,12 @@ pub async fn get_thumbnail(
let row = row.ok_or_else(|| ApiError::not_found("book not found"))?;
let thumbnail_path: Option<String> = row.get("thumbnail_path");
let data = if let Some(ref path) = thumbnail_path {
let (data, content_type) = if let Some(ref path) = thumbnail_path {
match std::fs::read(path) {
Ok(bytes) => bytes,
Ok(bytes) => {
let ct = detect_thumbnail_content_type(path);
(bytes, ct)
}
Err(_) => {
// File missing on disk (e.g. different mount in dev) — fall back to live render
crate::pages::render_book_page_1(&state, book_id, 300, 80).await?
@@ -626,7 +640,7 @@ pub async fn get_thumbnail(
};
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("image/webp"));
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),