An unreadable reglages_locaux.json, or a folder that refused to write it, refused the paper choice, so no badge sheet or preview could be produced on that machine. The chosen paper now holds for the session on that working folder, in memory, and the file is never overwritten. On that folder it wins over a stale paper still in the file; the first settings write that succeeds there retains it and withdraws the warning. The warning names the file and no longer promises defaults never written. Checked: node 3918, browser 773; each fix red under the previous code. --- FR --- [FIX] réglages locaux : un fichier illisible ou non écrit ne bloque plus Un reglages_locaux.json illisible, ou un dossier qui refusait de l'écrire, refusait le choix du papier : ni planche ni aperçu sur cette machine. Le papier choisi vaut désormais pour la séance sur ce dossier de travail, en mémoire, et le fichier n'est jamais écrasé. Sur ce dossier, il l'emporte sur un papier périmé du fichier ; la première écriture des réglages qui y réussit le retient et retire l'avertissement. L'avertissement nomme le fichier et ne promet plus de valeurs par défaut jamais écrites. Vérifié : node 3918, navigateur 773 ; chaque correction rouge avant elle. Assisted-by: Claude Opus 5.5
327 lines
14 KiB
JavaScript
327 lines
14 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Les textes des codes de src/stockage (§ 14.6) : le système de fichiers et le
|
|
// dépôt, les raisons d'un fichier illisible, les fautes et les raisons de
|
|
// dérive d'un placement, ce que l'ouverture annonce, le dossier de travail et
|
|
// les réglages locaux. Une raison qu'un texte ne traite pas à part ne lève
|
|
// jamais : une phrase générique la nomme, par son texte quand les tables en
|
|
// portent un, telle quelle sinon. Une cause que nomme le système, EROFS,
|
|
// ENOENT ou TypeMismatchError, n'est pas un code de l'application : les
|
|
// textes la citent telle quelle, entre parenthèses.
|
|
import {
|
|
capitale,
|
|
causeEntreParentheses,
|
|
cheminDe,
|
|
cite,
|
|
compte,
|
|
depuisQuand,
|
|
element,
|
|
enumeration,
|
|
numeros,
|
|
ouEcrire,
|
|
phraseDeRaison,
|
|
} from './aides.js';
|
|
|
|
// La raison d'un fichier d'état illisible, en une phrase.
|
|
function raisonIllisible({ raison, ...details }, texte) {
|
|
if (raison === 'ABSENT') return "Le fichier d'état manque, à côté de son historique.";
|
|
return phraseDeRaison(raison, details, texte);
|
|
}
|
|
|
|
// Les secours d'un fichier d'état illisible, quand l'erreur les nomme.
|
|
function secoursDe(secours) {
|
|
if (secours === undefined || secours === null) return '';
|
|
const offerts = [];
|
|
if (secours.precedent !== null) offerts.push(`la version précédente du fichier (révision ${secours.precedent})`);
|
|
if (secours.journal !== null) offerts.push(`le dernier instant de l'historique (révision ${secours.journal})`);
|
|
if (offerts.length === 0) return ' Aucune version de secours ne se lit.';
|
|
return ` ${capitale(enumeration(offerts))} ${offerts.length === 1 ? 'peut' : 'peuvent'} se rouvrir.`;
|
|
}
|
|
|
|
// Ce qu'une dérive touche, du retenu aux propositions, et l'accord du verbe.
|
|
function touchees({ ids, retenu }) {
|
|
const parties = [];
|
|
if (retenu) parties.push('le placement retenu');
|
|
if (ids.length === 1) parties.push(`la proposition n° ${ids[0]}`);
|
|
if (ids.length > 1) parties.push(`les propositions n° ${enumeration(ids.map(String))}`);
|
|
return { texte: capitale(enumeration(parties)), pluriel: (retenu ? 1 : 0) + ids.length > 1 };
|
|
}
|
|
|
|
// La personne d'une raison de dérive, par son nom affiché quand les détails
|
|
// le portent ; la table, par son numéro de même.
|
|
const laPersonne = ({ participant, personne = null }) => personne ?? `La personne n° ${participant}`;
|
|
const laTable = ({ table, numero = null }) => (numero === null ? `La table d'identifiant ${table}` : `La table ${numero}`);
|
|
|
|
export const TABLE = new Map([
|
|
// --- Le système de fichiers et le dépôt (src/stockage/types.js) ------------
|
|
[
|
|
'ABSENT',
|
|
({ chemin }) =>
|
|
typeof chemin === 'string' ? `Le fichier ${cite(chemin)} est introuvable.` : "Le fichier d'état manque.",
|
|
],
|
|
['ECRITURE', (details, texte) => `L'écriture de ${ouEcrire(details, texte)} a échoué ; le fichier d'avant reste intact.`],
|
|
['LECTURE', (details, texte) => `La lecture de ${ouEcrire(details, texte)} est refusée.`],
|
|
['EXISTE', ({ chemin }) => `Un fichier ${cite(chemin)} existe déjà.`],
|
|
['CHEMIN_REFUSE', ({ chemin }) => `Le chemin ${cite(String(chemin))} est refusé.`],
|
|
[
|
|
'ETAT_ILLISIBLE',
|
|
(details, texte) => {
|
|
const sujet = typeof details.base === 'string' ? `Le fichier d'état de ${cite(details.base)}` : 'Le fichier';
|
|
return `${sujet} ne s'ouvre pas. ${raisonIllisible(details, texte)}${secoursDe(details.secours)}`;
|
|
},
|
|
],
|
|
['CORRECTIF', ({ rang }) => `Un correctif de l'historique ne s'applique pas (opération ${rang}).`],
|
|
[
|
|
'CHEMIN_TROP_LONG',
|
|
({ racine }) =>
|
|
`Le dossier ${cite(cheminDe(racine))} est trop profond : aucun nom de fichier n'y tient ` +
|
|
'sous la longueur de chemin que Windows accepte.',
|
|
],
|
|
[
|
|
'IDENTIFIANT_PRESENT',
|
|
({ base }) =>
|
|
`Cet événement est déjà dans ce dossier, sous ${cite(base)} : ` +
|
|
"l'y placer une seconde fois en ferait deux copies de même identifiant.",
|
|
],
|
|
[
|
|
'VERROU_PRIS',
|
|
({ depuis, vivant }, texte) => {
|
|
if (vivant === true) {
|
|
return `Une autre séance modifie cet événement${depuisQuand(depuis, texte)} : il reste en lecture ici.`;
|
|
}
|
|
if (vivant === false) {
|
|
return (
|
|
`Une séance interrompue a laissé son verrou${depuisQuand(depuis, texte)}, et il n'a pas pu se reprendre : ` +
|
|
"l'événement reste en lecture."
|
|
);
|
|
}
|
|
return (
|
|
`Un verrou dont la séance ne se laisse pas vérifier — autre poste, verrou illisible, ou séance de ce poste dont la vie ne se prouve pas — tient cet événement${depuisQuand(depuis, texte)} : ` +
|
|
'il reste en lecture ici, et rien ne le reprend seul.'
|
|
);
|
|
},
|
|
],
|
|
[
|
|
'LECTURE_SEULE',
|
|
({ raison }, texte) => {
|
|
if (raison === 'BLOQUE') {
|
|
return `${texte('BLOQUE')} Rien ne peut changer ; « Débloquer » le rend de nouveau modifiable.`;
|
|
}
|
|
if (raison === 'FORMAT' || raison === 'FORMAT_PLUS_RECENT') {
|
|
return `${texte(raison)} Il s'ouvre en lecture seule, et aucun geste ne le modifie ici.`;
|
|
}
|
|
if (raison === 'ENCODAGE') {
|
|
return (
|
|
`Le fichier d'état a changé sur le disque. ${texte('ENCODAGE')} ` +
|
|
"L'événement reste en lecture seule ; rouvert par une version de secours, son premier geste " +
|
|
"range ce fichier dans la corbeille avant d'écrire l'état."
|
|
);
|
|
}
|
|
return `${phraseDeRaison(raison, {}, texte)} L'événement reste en lecture seule, et aucun geste ne le modifie ici.`;
|
|
},
|
|
],
|
|
[
|
|
'ETAT_NON_ECRIT',
|
|
(details, texte) => `Le geste est inscrit à l'historique, mais l'état ${ouEcrire(details, texte)} n'a pas pu s'écrire.`,
|
|
],
|
|
[
|
|
'CORBEILLE_SATUREE',
|
|
({ dossier }) =>
|
|
`La corbeille ${cite(dossier)} a reçu trop de suppressions dans la même seconde : ` +
|
|
'la suppression se refait une seconde plus tard.',
|
|
],
|
|
[
|
|
'COMPTEUR_SATURE',
|
|
({ compteur }) =>
|
|
`Les numéros ${compteur === 'proposition' ? 'de proposition' : `du compteur ${cite(compteur)}`} sont épuisés : ` +
|
|
'rien de neuf ne se numérote plus dans cet événement.',
|
|
],
|
|
['NON_DISPONIBLE', ({ cause }, texte) => `Cette commande n'est pas offerte ici${causeEntreParentheses(cause, texte)}.`],
|
|
|
|
// --- Les raisons d'un fichier illisible ------------------------------------
|
|
['VIDE', () => 'Le fichier est vide.'],
|
|
['JSON', () => "Le texte n'est pas du JSON valide."],
|
|
['FORME', ({ chemin }) => `Une valeur sort de sa règle${element(chemin)}.`],
|
|
['COMPTES', ({ chemin }) => `Un compte de l'en-tête contredit sa liste${element(chemin)}.`],
|
|
['REFERENCE', ({ chemin }) => `Une réservation ou un titre désigne ce qui n'existe pas${element(chemin)}.`],
|
|
['FORMAT_INCONNU', () => "Le format du fichier n'est pas reconnu."],
|
|
[
|
|
'FORMAT_PLUS_RECENT',
|
|
({ format }) => {
|
|
const quel = Number.isInteger(format) ? ` (format ${format})` : '';
|
|
return `Ce fichier vient d'une version plus récente du logiciel${quel}.`;
|
|
},
|
|
],
|
|
[
|
|
'ENCODAGE',
|
|
() => "Ses octets ne sont pas de l'UTF-8 : un éditeur l'a sans doute réenregistré dans un autre encodage.",
|
|
],
|
|
|
|
// --- Les fautes d'un placement, et ses raisons de dérive (§ 8.9, § 9) ------
|
|
[
|
|
'LONGUEUR',
|
|
({ tour, table, declare, ecrit }) => {
|
|
if (tour === null) return `Le placement déclare ${compte(declare, 'tour', 'tours')} et en porte ${ecrit}.`;
|
|
if (table === null) {
|
|
return `Au tour ${tour}, le placement déclare ${compte(declare, 'table', 'tables')} et en porte ${ecrit}.`;
|
|
}
|
|
return (
|
|
`Au tour ${tour}, la table ${table} reçoit ${compte(ecrit, 'personne', 'personnes')} ` +
|
|
`pour ${compte(declare, 'place', 'places')}.`
|
|
);
|
|
},
|
|
],
|
|
['DOUBLON', ({ tour, participant }) => `Au tour ${tour}, la personne n° ${participant} est placée plus d'une fois.`],
|
|
[
|
|
'INCONNU',
|
|
({ tour, participant }) => `Au tour ${tour}, la personne n° ${participant} est placée sans être déclarée.`,
|
|
],
|
|
['MANQUANT', ({ tour, participant }) => `Au tour ${tour}, la personne n° ${participant} n'est placée nulle part.`],
|
|
['IDENTIFIANT_REPETE', () => 'Une autre proposition porte déjà ce numéro.'],
|
|
['IDENTIFIANT_HORS_COMPTEUR', () => 'Ce numéro de proposition dépasse le compteur du fichier.'],
|
|
// Une raison de dérive nomme la personne par personne et la table par
|
|
// numero quand ses détails les portent (raisonsNommees, de l'application) ;
|
|
// sinon par leur identifiant.
|
|
['PARTICIPANT_EXCLU', (details) => `${laPersonne(details)} est exclue depuis.`],
|
|
['PARTICIPANT_SUPPRIME', (details) => `${laPersonne(details)} a été supprimée depuis.`],
|
|
['PARTICIPANT_NON_PLACE', (details) => `${laPersonne(details)}, ajoutée ou réintégrée depuis, n'y est pas placée.`],
|
|
['TABLE_SUPPRIMEE', (details) => `${laTable(details)} a été supprimée depuis.`],
|
|
['TABLE_AJOUTEE', (details) => `${laTable(details)} a été ajoutée depuis.`],
|
|
[
|
|
'CAPACITE_CHANGEE',
|
|
({ avant, maintenant, ...details }) => `${laTable(details)} est passée de ${avant} à ${compte(maintenant, 'place', 'places')}.`,
|
|
],
|
|
['TOURS_CHANGES', ({ avant, maintenant }) => `Le nombre de tours est passé de ${avant} à ${maintenant}.`],
|
|
|
|
// --- Ce que l'ouverture annonce (§ 8.6, § 8.8, § 8.9, § 9) -----------------
|
|
['RETENU_FAUTIF', () => 'Le placement retenu se contredit : la version précédente du fichier peut le reprendre.'],
|
|
['JOURNAL_REPRIS', () => "Le dernier geste, inscrit à l'historique mais pas dans l'état, est repris."],
|
|
[
|
|
'JOURNAL_EN_AVANCE_DE',
|
|
({ n }) =>
|
|
`L'historique compte ${compte(n, 'geste', 'gestes')} de plus que l'état : ` +
|
|
"l'état du disque est affiché, et le prochain geste inscrit une reprise.",
|
|
],
|
|
[
|
|
'JOURNAL_INCOMPLET',
|
|
({ etat, journal }) =>
|
|
`L'historique s'arrête à la révision ${journal}, l'état est à la révision ${etat} : ` +
|
|
'le prochain geste inscrit une reprise.',
|
|
],
|
|
[
|
|
'JOURNAL_ECARTEES',
|
|
({ n }) =>
|
|
`${compte(n, 'entrée', 'entrées')} de l'historique ${n <= 1 ? 'est écartée' : 'sont écartées'}, ` +
|
|
'à partir de la première ligne illisible.',
|
|
],
|
|
[
|
|
'JOURNAL_RECREE',
|
|
() =>
|
|
"Le fichier de l'historique avait disparu du dossier : il est réécrit en entier, " +
|
|
'tel que la séance le connaissait.',
|
|
],
|
|
[
|
|
'JOURNAL_FORMAT_PLUS_RECENT',
|
|
({ format }) =>
|
|
`L'historique vient d'une version plus récente du logiciel (format ${format}) : ` +
|
|
"il ne se lit pas, et l'événement reste en lecture seule.",
|
|
],
|
|
[
|
|
'JOURNAL_ILLISIBLE',
|
|
({ raison }, texte) =>
|
|
`L'historique à côté de ce fichier ne se lit pas. ${phraseDeRaison(raison, {}, texte)} ` +
|
|
"Il est laissé tel quel, et le prochain geste commence l'historique dans un nouveau fichier.",
|
|
],
|
|
[
|
|
'JOURNAL_ETRANGER',
|
|
({ evenement }) =>
|
|
`L'historique à côté de ce fichier appartient à un autre événement (${evenement}) : il est ignoré.`,
|
|
],
|
|
[
|
|
'PROPOSITIONS_FAUTIVES',
|
|
({ ids }) =>
|
|
ids.length <= 1
|
|
? `Une proposition incohérente est écartée (${numeros(ids)}).`
|
|
: `${ids.length} propositions incohérentes sont écartées (${numeros(ids)}).`,
|
|
],
|
|
[
|
|
'PROPOSITIONS_MANQUANTES',
|
|
({ n }) =>
|
|
`${compte(n, "proposition que l'en-tête annonce manque", "propositions que l'en-tête annonce manquent")} ` +
|
|
'dans le fichier : celles qu\'il porte sont gardées.',
|
|
],
|
|
[
|
|
'DERIVE',
|
|
(details) => {
|
|
const { texte, pluriel } = touchees(details);
|
|
return pluriel
|
|
? `${texte} ne décrivent plus la liste ou les tables actuelles : ils sont gardés, et rien n'est effacé.`
|
|
: `${texte} ne décrit plus la liste ou les tables actuelles : il est gardé, et rien n'est effacé.`;
|
|
},
|
|
],
|
|
|
|
// --- Le dossier de travail, les réglages locaux (§ 8.5, § 8.6, § 8.8) ------
|
|
['NON_PUBLIE', () => "Le dossier de l'exécutable n'est pas connu : les événements se rangent dans les Documents."],
|
|
[
|
|
'DONNEES_APPLICATIVES',
|
|
() => "L'exécutable est rangé sous les données applicatives : les événements se rangent dans les Documents.",
|
|
],
|
|
[
|
|
'SONDE_ECHOUEE',
|
|
({ cause }, texte) =>
|
|
`Le dossier data à côté de l'exécutable refuse l'écriture${causeEntreParentheses(cause, texte)} : ` +
|
|
'les événements se rangent dans les Documents.',
|
|
],
|
|
// Une cause de la sonde, que les textes citent entre parenthèses.
|
|
['RELECTURE_DIFFERENTE', () => "le fichier d'essai relu ne porte pas ce qui vient d'y être écrit"],
|
|
// Une cause de l'OPFS : sous des paramètres régionaux qui ne sont pas
|
|
// UTF-8, le navigateur ne nomme pas un dossier ou un fichier hors ASCII.
|
|
[
|
|
'NOM_NON_CONVERTI',
|
|
() =>
|
|
"le navigateur ne sait pas nommer un dossier ou un fichier aux caractères accentués sous les réglages de langue du système",
|
|
],
|
|
[
|
|
'PORTABLE_NON_INSCRIPTIBLE',
|
|
({ racine, evenements, cause }, texte) => {
|
|
const porte =
|
|
evenements === null
|
|
? 'porte peut-être des événements'
|
|
: `porte ${compte(evenements, 'événement', 'événements')}`;
|
|
return `Le dossier ${cite(cheminDe(racine))} ${porte}, mais refuse l'écriture${causeEntreParentheses(cause, texte)}.`;
|
|
},
|
|
],
|
|
[
|
|
'DOCUMENTS_NON_INSCRIPTIBLE',
|
|
({ racine, cause }, texte) =>
|
|
`Le dossier de travail ${cite(cheminDe(racine))} refuse l'écriture${causeEntreParentheses(cause, texte)} : ` +
|
|
"aucun geste ne s'y enregistrerait.",
|
|
],
|
|
[
|
|
'PLATEFORME_WEB',
|
|
() =>
|
|
"Sous la plateforme web, ni le renommage atomique ni le verrou n'existent : " +
|
|
'une panne peut laisser un fichier incomplet, et deux onglets peuvent écrire le même événement.',
|
|
],
|
|
// dossier, quand il est donné, nomme le dossier où la séance écrit
|
|
// désormais (ecrireAilleurs) ; sans lui, le dossier de travail.
|
|
[
|
|
'SUPPORT_AMOVIBLE',
|
|
({ dossier }) =>
|
|
`${typeof dossier === 'string' ? `Le dossier ${cite(dossier)}` : 'Le dossier de travail'} ` +
|
|
'est sur un support amovible : retirer le support pendant une écriture peut la laisser incomplète.',
|
|
],
|
|
[
|
|
'REGLAGES_ILLISIBLES',
|
|
({ chemin, raison }, texte) => {
|
|
const pourquoi =
|
|
raison === 'JSON' || raison === 'FORME' ? texte(raison) : `La lecture est refusée (${raison}).`;
|
|
return (
|
|
`Les réglages locaux ${cite(chemin)} ne se lisent pas. ${pourquoi} Le fichier reste tel quel : ` +
|
|
'le papier choisi vaut pour cette séance, sans être retenu sur cette machine, ' +
|
|
"et le cadrage n'est pas retenu."
|
|
);
|
|
},
|
|
],
|
|
]);
|