gestion_table_tournante_libre/electron/main.js
Mathieu Benoit d5a1560915 [FIX] core: bounded geometry, dated lock, empty seat, neighbour lists
Non-finite scale ratios, view translations and handle counts now raise
instead of returning 0, NaN or looping forever. The lock carries the
instant from the single clock, a lock taken back in this session's name is
its own, and quitting releases held locks. An empty chair is null only, at
its exact path. A table grown over a neighbour warns LISTE_SUR_VOISINE once
per pair; successive remedies never aim at the same free cell.
Checked: every new test red first; 2983 node and 617 browser tests green
from the index alone.

--- FR ---

[FIX] cœur : géométrie bornée, verrou daté, chaise vide, listes voisines

Un rapport d'échelle, une translation de vue ou un compte de poignée non
finis lèvent au lieu de rendre 0, NaN ou de boucler sans fin. Le verrou
porte l'instant de l'horloge unique, un verrou repris au nom de la séance
est le sien, et quitter rend les verrous tenus. Une chaise vide est null
seulement, à son chemin exact. Une table agrandie sur sa voisine avertit
LISTE_SUR_VOISINE une fois par paire ; les remèdes successifs ne visent
jamais la même case libre.
Vérifié : chaque épreuve rouge d'abord ; 2983 node et 617 navigateur verts
depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-09 04:00:22 -04:00

120 lines
5.4 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Processus principal de la coquille Electron (§ 13.1) : une fenêtre qui
// charge la page construite par Vite dans www/, et le système de fichiers de
// la plateforme (electron/fichiers.js), offert à la page par un canal IPC
// par primitive, que electron/preload.cjs expose sous window.gtt.fichiers ;
// un canal de plus, gtt:copier, remet un texte au presse-papiers du système
// (window.gtt.copier).
//
// La page n'a ni Node ni accès au disque : isolation de contexte active,
// intégration de Node coupée, rendu en bac à sable. Elle n'atteint le
// processus principal que par ces canaux, et n'y désigne un fichier que par
// une racine que ce processus connaît et un chemin relatif à elle. Un nom
// importé interprété comme du balisage reste ainsi une faute d'affichage, et
// non un accès au disque.
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell } from 'electron';
import { mkdtempSync, rmSync } from 'node:fs';
import { readdir, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { creerFichiers, decrireSysteme, processusVivant, servir } from './fichiers.js';
// Chemins relatifs à ce module : ils valent dans le dépôt comme dans
// l'archive app.asar de l'exécutable, qui reproduit electron/ et www/.
const PRECHARGEMENT = fileURLToPath(new URL('./preload.cjs', import.meta.url));
const PAGE = fileURLToPath(new URL('../www/index.html', import.meta.url));
// Profil de Chromium (§ 8.6) : un dossier neuf sous le dossier temporaire du
// système, nommé gtt-profil-<pid>-…, posé avant que l'application soit
// prête — Chromium l'ouvre alors —, et retiré à la sortie. L'application ne
// garde rien dans ce profil, et un exécutable portable ne laisse ainsi rien
// dans AppData, ni dans le dossier de travail.
//
// Le retrait de la sortie n'est qu'un premier temps : Chromium écrit encore
// dans le profil après l'évènement quit — son état local, ses préférences,
// le stockage de session —, et le dossier renaît, réduit à quelques fichiers.
// Une séance qui s'arrête sans sortir n'en retire rien. Chaque démarrage
// retire donc, après l'ouverture de la fenêtre, les profils des séances
// terminées : le pid de leur nom ne désigne plus aucun processus de ce
// poste. Le profil d'une séance qui tourne encore reste.
const TEMPORAIRE = app.getPath('temp');
const PROFIL_DE_SEANCE = /^gtt-profil-(\d+)-/;
const PROFIL = mkdtempSync(join(TEMPORAIRE, `gtt-profil-${process.pid}-`));
app.setPath('userData', PROFIL);
app.on('quit', () => {
try {
rmSync(PROFIL, { recursive: true, force: true });
} catch {
// Au mieux : le démarrage suivant le retire.
}
});
async function retirerProfilsTermines() {
for (const nom of await readdir(TEMPORAIRE)) {
const pid = Number(PROFIL_DE_SEANCE.exec(nom)?.[1]);
if (!(pid > 0) || pid === process.pid || processusVivant(pid)) continue;
await rm(join(TEMPORAIRE, nom), { recursive: true, force: true }).catch(() => {});
}
}
// Le système de fichiers : emplacements et racines lus une fois, au
// démarrage (§ 8.6) ; les dialogues s'ouvrent sur la fenêtre de la page. Les
// gestionnaires sont posés avant le chargement de la page, qui peut les
// appeler dès son premier script.
let fenetre = null;
const fichiers = creerFichiers({
...decrireSysteme({
env: process.env,
plateforme: process.platform,
cheminSysteme: (nom) => app.getPath(nom),
nomProduit: app.getName(),
}),
dialog,
shell,
fenetre: () => fenetre,
});
servir(fichiers, (canal, gestionnaire) => ipcMain.handle(canal, gestionnaire));
// Le presse-papiers : le texte reçu, une chaîne, remplace son contenu.
ipcMain.handle('gtt:copier', (_evenement, texte) => {
if (typeof texte !== 'string') throw new TypeError("le texte à copier n'est pas une chaîne");
clipboard.writeText(texte);
});
// Les verrous que la page a pris et pas rendus s'effacent à la sortie (§ 8.8) :
// la page ne survit pas au processus principal, et une fermeture de la fenêtre
// ne lui laisse pas le temps de les rendre elle-même. rendreVerrousTenus les
// relit et les efface de façon synchrone, comme le retrait du profil, chacun
// seulement s'il porte encore la séance qui l'a pris.
app.on('quit', () => fichiers.rendreVerrousTenus());
function creerFenetre() {
fenetre = new BrowserWindow({
width: 1366,
height: 768,
webPreferences: {
preload: PRECHARGEMENT,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
// La page ne quitte jamais www/ : toute nouvelle fenêtre est refusée, toute
// navigation annulée, quelle que soit sa cible.
fenetre.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
fenetre.webContents.on('will-navigate', (evenement) => evenement.preventDefault());
fenetre.loadFile(PAGE);
}
// Sans menu posé par l'application, Electron installe le sien : en anglais,
// alors que l'interface est en français (§ 2), et porteur de « Reload » et
// des outils de développement. null le retire, et retire avec lui la barre
// de menu des fenêtres sous Windows et Linux ; il est posé avant la première
// fenêtre, qui naîtrait sinon avec la barre.
app.whenReady().then(() => {
Menu.setApplicationMenu(null);
creerFenetre();
retirerProfilsTermines().catch(() => {});
});