Dis à l'application quelle version elle est, et sous quelle licence

Page « À propos » (/a-propos) : manuel annoncé à venir, contact, site de
l'auteur, version publiée avec sa date de build, et licence AGPL v3 avec le
lien vers le dépôt — l'AGPL attend que les utilisateurs d'un service en réseau
puissent en obtenir la source, ce lien n'est donc pas un ornement.

L'application ne connaissait pas sa version : elle vivait dans le manifeste du
paquet et dans les tags git, jamais dans le binaire. `build/publier-release.sh`
pose désormais `-p:Version` et `-p:MaBibliDateBuild` ; l'API rend les deux par
`GET /api/version`.

⚠️ L'horodatage est le TÉMOIN de l'injection, et rien ne le calcule côté
MSBuild. Sans lui, la version lue serait le « 1.0.0 » que le SDK pose par
défaut : il se lirait comme une vraie version alors qu'il ne désigne rien, et
c'est exactement la valeur qu'on ira chercher pour diagnostiquer un appareil au
cache dépareillé. Mieux vaut ne rien annoncer — un binaire compilé à la main se
déclare « version de développement », ce qui est vrai.

Autres décisions :

- l'entrée du menu est DÉTACHÉE des six destinations, par un filet au-dessus
  sur téléphone et à gauche en rangée sur PC : « À propos » est une annexe, pas
  une septième destination. Ses règles vivent en feuille GLOBALE, comme tout le
  menu — une règle scopée n'atteint pas ce que rend un NavLink ;
- le « mailto: » reste un lien même hors-ligne : il ne charge aucune page et
  passe la main au client de messagerie, qui sait mettre un message en attente.
  Le site et le dépôt, eux, basculent en boutons désactivés portant leur motif,
  comme les liens d'export des envies ;
- la version est un sixième instantané hors-ligne : on la lit justement quand
  quelque chose ne va pas, et un appareil qu'on soupçonne est souvent celui qui
  n'a plus de réseau. L'écran dit alors que c'est la dernière version vue du
  serveur.

Le point de rupture de 40 rem reste identique dans les deux feuilles, et
/a-propos est inscrite dans la table de remontée des routes.

Vérifié en exécution, l'API lancée : sans injection
`{"numero":null,"publiee":false}`, avec `-p:Version=0.4.1
-p:MaBibliDateBuild=…` `{"numero":"0.4.1","publiee":true}`. Le rendu des écrans
n'a PAS été vérifié en navigateur. 618 tests au vert (605 avant).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-21 14:50:34 +02:00
co-authored by Claude Opus 5
parent 0976da90be
commit 498579f27b
13 changed files with 449 additions and 0 deletions
+13
View File
@@ -99,6 +99,19 @@
<NavLink class="menu-lien" href="revues">Revues</NavLink>
<NavLink class="menu-lien" href="prets">Prêts</NavLink>
<NavLink class="menu-lien" href="souhaits">Envies</NavLink>
@*
« À propos » n'est PAS une septième destination : c'est une annexe, et elle est
visuellement détachée pour cela. Les six entrées ci-dessus sont la bibliothèque ; celle-ci
porte le manuel, le contact, la version et la licence — on y va une fois, pas tous les
jours. Mise sur le même rang, elle diluerait une navigation qu'on venait justement de
reprendre. Elle reste dans le menu plutôt qu'en pied de page : sur téléphone, un pied de
page vit sous une liste de trois cents livres.
⚠️ Elle reste active hors-ligne, comme les six autres : la version qu'on vient y lire est
justement ce qu'on cherche quand quelque chose ne va pas.
*@
<NavLink class="menu-lien menu-lien-annexe" href="a-propos">À propos</NavLink>
</nav>
</div>
+150
View File
@@ -0,0 +1,150 @@
@page "/a-propos"
@inject ServiceLivresApi Api
@inject EtatReseau Reseau
@implements IDisposable
@*
Ce que l'application dit d'elle-même : à qui écrire, où trouver de l'aide, quelle version
tourne réellement, et sous quelle licence elle est distribuée.
⚠️ Ce n'est pas une page d'ornement. Elle porte la version, c'est-à-dire la première chose
qu'on demande à quelqu'un dont l'application se comporte bizarrement — le projet a déjà connu
un appareil au cache dépareillé, où seule une version affichée aurait tranché. Et elle porte
le lien vers la source, que l'AGPL v3 attend qu'un service en réseau offre à ses utilisateurs.
*@
<PageTitle>MaBibli — à propos</PageTitle>
<h1 class="titre-page">À propos</h1>
<section class="bloc-apropos">
<h2 class="titre-section">Manuel</h2>
<p class="message-discret">
Un manuel d'utilisation est <strong>à venir</strong>. En attendant, chaque écran porte ses
explications à l'endroit où elles servent.
</p>
</section>
<section class="bloc-apropos">
<h2 class="titre-section">Contact</h2>
@*
⚠️ Le « mailto: » reste un lien même hors-ligne, et c'est délibéré : il ne charge aucune
page, il passe la main au client de messagerie de l'appareil, lequel sait mettre un
message en attente d'envoi. Le désactiver empêcherait précisément d'écrire « ça ne marche
pas, je n'ai plus de réseau ».
*@
<p><a href="mailto:info@limonier.be">info@limonier.be</a></p>
@*
Le site, lui, est une vraie navigation vers l'extérieur : hors-ligne elle finirait sur la
page d'erreur du navigateur, hors de l'application. Un <a> ne se désactivant pas, il
bascule en bouton inerte portant sa raison — même règle que les liens d'export des envies.
*@
@if (Reseau.EnLigne)
{
<p>
<a href="@Site" target="_blank" rel="noopener">www.limonier.be</a>
</p>
}
else
{
<p>
<button type="button" class="bouton bouton-discret" disabled
title="@EtatReseau.MotifHorsLigne">www.limonier.be</button>
</p>
}
</section>
<section class="bloc-apropos">
<h2 class="titre-section">Version</h2>
@if (_version is null)
{
<p class="message-discret">Chargement…</p>
}
else if (_version.Publiee)
{
<p>
<strong>@_version.Numero</strong>
<span class="message-discret"> — compilée @Quand(_version.DateBuild!.Value)</span>
</p>
@if (!Reseau.EnLigne)
{
<p class="message-discret">
Hors ligne : c'est la dernière version annoncée par le serveur, pas forcément
celle qui y tourne en ce moment.
</p>
}
}
else
{
@*
⚠️ Ne JAMAIS afficher ici le « 1.0.0 » que le SDK .NET pose par défaut : il se lirait
comme une vraie version. Mieux vaut ne rien annoncer que d'annoncer un faux numéro,
surtout sur la valeur qui sert à diagnostiquer.
*@
<p class="message-discret">
Version de développement : ce binaire n'a pas été produit par la chaîne de
publication, il n'a donc pas de numéro de version à annoncer.
</p>
}
</section>
<section class="bloc-apropos">
<h2 class="titre-section">Licence</h2>
<p>
MaBibli est distribuée sous licence <strong>GNU AGPL v3</strong>.
</p>
@*
Le lien vers le dépôt n'est pas un ornement : l'AGPL demande que les utilisateurs d'un
service accessible par le réseau puissent en obtenir le code source.
*@
@if (Reseau.EnLigne)
{
<p>
Son code source est disponible :
<a href="@Depot" target="_blank" rel="noopener">@Depot</a>
</p>
}
else
{
<p>
Son code source est disponible à l'adresse <code>@Depot</code>
<button type="button" class="bouton bouton-discret" disabled
title="@EtatReseau.MotifHorsLigne">Ouvrir le dépôt</button>
</p>
}
</section>
@code {
/// <summary>Site de l'auteur, ouvert dans une autre fenêtre.</summary>
private const string Site = "https://www.limonier.be";
/// <summary>Dépôt du code — ce que l'AGPL attend qu'on rende accessible.</summary>
private const string Depot = "https://git.akbar.nohost.me/mathieu/mabibli";
private VersionApplication? _version;
protected override async Task OnInitializedAsync()
{
Reseau.Change += SurChangementReseau;
_version = await Api.ObtenirVersionAsync();
}
/// <summary>
/// Au retour du réseau, la version se redemande : hors-ligne on affichait la dernière connue.
/// </summary>
private void SurChangementReseau() => _ = InvokeAsync(async () =>
{
_version = await Api.ObtenirVersionAsync();
StateHasChanged();
});
private static string Quand(DateTimeOffset instant)
{
var local = instant.ToLocalTime();
return $"le {local:dd/MM/yyyy} à {local:HH:mm}";
}
public void Dispose() => Reseau.Change -= SurChangementReseau;
}
+11
View File
@@ -19,6 +19,17 @@ public static class ClesCache
public const string Utilisateur = "utilisateur";
/// <summary>
/// La version publiée et sa date de build, telles que le serveur les a annoncées.
/// </summary>
/// <remarks>
/// ⚠️ Elle vaut la peine d'être rangée précisément parce qu'on la lit quand quelque chose ne
/// va pas — et un appareil dont on soupçonne le cache est aussi bien celui qui n'a plus de
/// réseau. Ce qui est affiché hors-ligne est la <b>dernière version vue du serveur</b>, et
/// l'écran le dit : c'est un fait daté, pas une supposition.
/// </remarks>
public const string Version = "version";
/// <summary>
/// La liste d'envies de l'utilisateur courant.
/// </summary>
@@ -76,6 +76,10 @@ public static class RemonteeRoutes
("/auteurs", Racine),
("/prets", Racine),
// « À propos » est une annexe du menu : elle remonte au catalogue, comme toute racine de
// branche. Elle n'a pas d'enfants, et n'en aura pas.
("/a-propos", Racine),
("/not-found", Racine),
];
@@ -53,6 +53,7 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
await ListerSeriesAsync(ct);
await ListerRevuesAsync(ct);
await ObtenirUtilisateurAsync(ct);
await ObtenirVersionAsync(ct);
return reseau.EnLigne;
}
catch (OperationCanceledException)
@@ -459,6 +460,30 @@ public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, Etat
return instantane?.Donnees ?? UtilisateurCourant.Anonyme;
}
/// <summary>
/// Version publiée du serveur, avec repli sur la dernière connue de cet appareil.
/// </summary>
/// <remarks>
/// Même forme que <see cref="ObtenirUtilisateurAsync"/>. Hors-ligne on rend l'instantané
/// plutôt que rien : c'est un fait daté (« voilà ce que le serveur annonçait »), et c'est
/// justement quand l'application se comporte mal qu'on veut ce numéro. L'écran distingue les
/// deux cas ; ici on ne fabrique aucune valeur.
/// </remarks>
public async Task<VersionApplication> ObtenirVersionAsync(CancellationToken ct = default)
{
var (ok, version) = await EssayerAsync(
() => http.GetFromJsonAsync<VersionApplication>("api/version", Json, ct));
if (ok && version is not null)
{
await MemoriserAsync(ClesCache.Version, version);
return version;
}
var instantane = await LireCacheAsync<VersionApplication>(ClesCache.Version);
return instantane?.Donnees ?? VersionApplication.Developpement;
}
/// <summary>
/// Exécute un appel de lecture. Renvoie <c>false</c> — sans exception — quand le réseau
/// manque, ce qui est la bascule vers le cache. Une annulation demandée par l'appelant,
+23
View File
@@ -1486,6 +1486,19 @@ body {
border-left-color: #f5d76e;
}
/*
« À propos » est une annexe, pas une septième destination : un filet la sépare des six
entrées de la bibliothèque, et elle se porte en retrait. Elle reste un lien de plein droit —
même cible tactile, même état actif — parce qu'on la touche au pouce comme les autres.
*/
.menu-lien-annexe {
margin-top: 0.35rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(232, 237, 243, 0.8);
font-weight: 400;
font-size: 0.9rem;
}
/*
À partir de 40rem la rangée tient sans se comprimer, et la bascule n'a plus
de raison d'être. Le point de rupture est répété dans `MainLayout.razor.css`
@@ -1526,6 +1539,16 @@ body {
background: rgba(255, 255, 255, 0.12);
border-bottom-color: #f5d76e;
}
/* En rangée, un filet au-dessus ne séparerait rien : le trait passe à GAUCHE, et la marge
supérieure disparaît sous peine de désaligner l'entrée du reste de la rangée. */
.menu-lien-annexe {
margin-top: 0;
margin-left: 0.4rem;
padding-left: 0.9rem;
border-top: 0;
border-left: 1px solid rgba(255, 255, 255, 0.25);
}
}
/* --- Attente (lot L1) -------------------------------------------------