gestion_table_tournante_libre/electron/main.js
Mathieu Benoit 8b7c732399 [ADD] platform: print bridge, atomic byte writes, badge sheet file name
Printed views and the badge sheet leave the app through the platform. Under
Electron a gtt:imprimer channel prints with the paper and orientation asked
for and reports sent, cancelled, failed or unknown; an exception is logged
to the console and never shown raw to the operator. Every file system,
real or simulated, writes bytes atomically from a copy taken at the call.
The sheet name carries event, date and imposition, and a failed print
becomes IMPRESSION_ECHEC.
Checked: red first, twelve mutations caught; 3502 node, 659 browser and 79
long tests green from the index alone.

--- FR ---

[ADD] plateforme : pont d'impression, octets atomiques, nom de planche

Les vues imprimables et la planche sortent de l'application par la
plateforme. Sous Electron, un canal gtt:imprimer imprime au papier et à
l'orientation demandés et rend envoyé, annulé, échec ou inconnu ; une
exception va à la console, jamais brute à l'opérateur. Chaque système de
fichiers, réel ou simulé, écrit des octets de façon atomique, depuis une
copie prise à l'appel. Le nom de la planche porte événement, date et
imposition, et une impression échouée devient IMPRESSION_ECHEC.
Vérifié : rouge d'abord, douze mutations attrapées ; 3502 node, 659
navigateur et 79 longues vertes depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-09 15:26:13 -04:00

127 lines
5.8 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) ; un dernier, gtt:imprimer, imprime la page sur le
// papier et dans l'orientation qu'elle demande (electron/impression.js,
// window.gtt.imprimer).
//
// 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';
import { servirImpression } from './impression.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);
});
// L'impression : la page qui la demande s'imprime, par la boîte de dialogue
// du système, sur le papier et dans l'orientation reçus, et rien d'autre.
servirImpression((canal, gestionnaire) => ipcMain.handle(canal, gestionnaire));
// 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(() => {});
});