Files
mabibli/MaBibli.Client/Pages/Auteurs.razor
T
mathieuandClaude Opus 5 ebc5f95d49 Consulter la bibliotheque hors-ligne depuis un cache IndexedDB
Le piege que CLAUDE.md signale : en Blazor WebAssembly le code tourne dans le
navigateur alors que SQLite vit sur le serveur, et le service worker ne met en
cache que les assets. Sans travail explicite, l'application demarre hors-ligne
et affiche une bibliotheque vide.

- js/cache-hors-ligne.js : instantanes JSON dans IndexedDB, et surveillance des
  bascules online/offline. Aucune logique metier.
- CacheHorsLigne / EtatReseau : lecture-ecriture des instantanes, et etat reseau
  combinant navigator.onLine (fiable seulement par sa negation) avec le sort
  reel des appels HTTP.
- ServiceLivresApi : les lectures retombent sur le cache, les ecritures sont
  refusees. Le catalogue entier est memorise, pas les reponses filtrees : c'est
  ce qui rend la recherche hors-ligne possible sur tout le fonds.
- FiltreLivresLocal : pendant navigateur de FiltreLivres, avec un test qui
  confronte les deux implementations sur les memes donnees.
- Interface : pastille et bandeau d'etat avec la date de synchronisation, et
  actions d'ecriture desactivees avec leur raison plutot que boutons morts.
- js/mise-a-jour.js : les empreintes WASM etant desactivees, le service worker
  est le seul cache-busting du projet. L'enregistrement journalise desormais ses
  echecs, et un bandeau propose la nouvelle version sans attendre la fermeture
  de tous les onglets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:59:19 +02:00

148 lines
5.2 KiB
Plaintext

@page "/auteurs"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@implements IDisposable
<PageTitle>MaBibli — auteurs</PageTitle>
<h1 class="titre-page">Auteurs</h1>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_suggestions is { Count: > 0 })
{
@*
Ce qui arrive ici est ce que la machine REFUSE de trancher. Les variantes sûres — casse,
accents, ordre « Nom, Prénom », initiales abrégées — ont déjà été réunies à l'écriture et
n'apparaissent jamais dans cette liste. Restent les cas ambigus, typiquement un patronyme
seul face à un nom complet : deux personnes d'une même famille écrivent aussi des livres.
*@
<section class="rapprochements">
<h2 class="titre-section">Rapprochements possibles</h2>
<p class="message-discret">
Ces noms se ressemblent sans qu'on puisse l'affirmer. Un patronyme seul peut désigner
une autre personne de la même famille — à vous de trancher. Une fusion se défait mal.
</p>
<ul class="liste-rapprochements">
@foreach (var suggestion in _suggestions)
{
<li class="carte-rapprochement" @key="(suggestion.Source.Id, suggestion.Cible.Id)">
<p class="rapprochement-noms">
<strong>@suggestion.Source.Nom</strong>
(@Livres(suggestion.Source.NombreDeLivres))
est-il <strong>@suggestion.Cible.Nom</strong>
(@Livres(suggestion.Cible.NombreDeLivres)) ?
</p>
<div class="actions-formulaire">
@* Une fusion est une écriture : elle demande le réseau, comme le reste. *@
<button type="button" class="bouton bouton-principal"
disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
@onclick="() => AccepterAsync(suggestion)">
Oui, réunir sous « @suggestion.Cible.Nom »
</button>
<button type="button" class="bouton bouton-discret"
disabled="@(_enCours || !Reseau.EnLigne)" title="@MotifBlocage"
@onclick="() => RefuserAsync(suggestion)">
Non, deux personnes
</button>
</div>
</li>
}
</ul>
</section>
}
@if (_auteurs is null)
{
<p class="message-discret">Chargement…</p>
}
else if (_auteurs.Count == 0)
{
<p class="message-discret">Aucun auteur : la bibliothèque est vide.</p>
}
else
{
<p class="compteur">@_auteurs.Count auteur@(_auteurs.Count > 1 ? "s" : "")</p>
<ul class="liste-auteurs">
@foreach (var auteur in _auteurs)
{
<li class="carte-auteur-ligne" @key="auteur.Id">
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
</li>
}
</ul>
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
</nav>
@code {
private IReadOnlyList<AuteurDto>? _auteurs;
private IReadOnlyList<SuggestionRapprochement>? _suggestions;
private bool _enCours;
private string? _erreur;
protected override void OnInitialized() => Reseau.Change += SurChangementReseau;
private void SurChangementReseau() => _ = InvokeAsync(async () =>
{
await ChargerAsync();
StateHasChanged();
});
public void Dispose() => Reseau.Change -= SurChangementReseau;
/// <summary>Raison du blocage des fusions, ou <c>null</c> en ligne.</summary>
private string? MotifBlocage => Reseau.EnLigne ? null : EtatReseau.MotifHorsLigne;
protected override Task OnInitializedAsync() => ChargerAsync();
private static string Livres(int nombre) => $"{nombre} livre{(nombre > 1 ? "s" : "")}";
private async Task ChargerAsync()
{
_erreur = null;
try
{
_auteurs = await Api.ListerAuteursAsync();
_suggestions = await Api.ListerRapprochementsAsync();
}
catch (Exception ex)
{
_erreur = $"Impossible de charger les auteurs : {ex.Message}";
}
}
private Task AccepterAsync(SuggestionRapprochement suggestion) =>
DeciderAsync(() => Api.AccepterRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));
private Task RefuserAsync(SuggestionRapprochement suggestion) =>
DeciderAsync(() => Api.RefuserRapprochementAsync(suggestion.Source.Id, suggestion.Cible.Id));
private async Task DeciderAsync(Func<Task<string?>> action)
{
_enCours = true;
var erreur = await action();
_enCours = false;
if (erreur is not null)
{
_erreur = erreur;
return;
}
// Une fusion peut en rendre d'autres caduques (l'un des deux noms disparaît) : on relit
// la liste complète plutôt que de retirer la ligne traitée à la main.
await ChargerAsync();
}
}