À lire en premier si tu es une IA qui reprend ce projet. Ce fichier contient tout le nécessaire pour continuer sans rien redécouvrir. Le README.md contient les commandes et les règles de code ; ce fichier-ci contient l'historique des décisions, les pièges connus et ce qui reste à faire.
npm install # première fois seulement
npm run verifier # doit passer avant et après toute modification
npm run dev # lance l'applicationSi npm run verifier échoue avant même que tu aies touché au code, arrête-toi et
signale-le : quelque chose a été cassé entre-temps.
Emplacement du projet : C:\Users\colin\Desktop\Gestion\APP
Données de l'utilisateur : %APPDATA%\Ohmnia\ (jamais dans le projet)
L'utilisateur travaille dans l'Invite de commandes (
cmd.exe), pas PowerShell. Quand tu lui donnes une commande, utilise la syntaxe cmd ou précise le terminal. Exemple :set VAR=valeur(cmd) et non$env:VAR = "valeur"(PowerShell). Repère :C:\...>= cmd ·PS C:\...>= PowerShell.
Ohmnia est une application de gestion pour indépendant (à l'origine un technicien électronicien en Valais, Suisse). Elle remplace un fichier Excel/VBA fragile.
- Desktop, 100 % local. Aucune donnée ne quitte l'ordinateur. Le seul accès réseau possible est la vérification de mise à jour, et il est désactivé par défaut.
- Pile : Electron 43 + React 19 + TypeScript + SQLite (
node:sqlite), empaquetée en.exeWindows parelectron-builder. - Langue de travail : tout est en français — code, commentaires, noms de variables, noms de colonnes SQL. L'interface est traduisible FR/EN.
- Le code doit rester simple à relire et à modifier par lui. Il n'est pas développeur professionnel. Pas d'ORM, pas de state manager, pas d'abstraction inutile. SQL écrit à la main et lisible.
- Fiable, sécurisé et optimisé, mais sans sacrifier le point 1.
- Messages d'erreur en français, précis et compréhensibles — jamais un message technique brut.
- Toutes les divisions protégées contre zéro. Aucune donnée d'exemple comptée dans les totaux.
| Décision | Raison |
|---|---|
node:sqlite et pas better-sqlite3 |
better-sqlite3 exige Visual Studio Build Tools (absents de la machine) et une recompilation à chaque montée de version d'Electron. node:sqlite est intégré à Node 24 — zéro dépendance native. |
| Preload compilé en CommonJS | Un preload sandbox: true ne peut pas charger de vrai module ESM. Forcé via output.format: 'cjs' dans electron.vite.config.ts. Le fichier produit est index.js, pas index.mjs. |
app.setName('Ohmnia') explicite |
Sans cela, le dossier de données changeait selon le mode de lancement (Electron au lieu de Ohmnia). Bug réel rencontré. |
| Migration de dossier de données | L'app s'appelait gestion-electronicien avant d'être renommée. src/main/db/migration-dossier.ts reprend automatiquement l'ancien dossier. |
Pas de clé étrangère sur reference_inventaire |
La spec veut qu'on puisse facturer un article absent de l'inventaire (simple avertissement). La contrainte bloquait tout enregistrement — bug réel signalé par l'utilisateur. |
| Checkpoint WAL avant toute copie de la base | Sans lui, la copie ne contenait même pas les tables. Les sauvegardes étaient inutilisables. Bug grave trouvé en test. |
Modale interne au lieu de window.prompt() |
Electron n'implémente pas prompt(). alert() et confirm() fonctionnent. |
- Sauvegardes vides (grave). Mode WAL : les écritures récentes vivent dans
gestion.sqlite-wal. Copier seulementgestion.sqliteproduisait une copie sans même les tables. →viderJournalWal()avant toute copie. FOREIGN KEY constraint failedà l'enregistrement d'une facture. Contrainte surfacture_lignes.reference_inventaire. → Retirée par migration.- Import bancaire lisant la colonne Solde au lieu du Montant. → Lecture des en-têtes (Débit/Crédit/Montant/Solde), avec repli si pas d'en-tête.
prompt() is not supported→ composantModale.- Tableaux larges coupés quand la fenêtre n'est pas en plein écran.
→
overflow-x: autosur.carte+minWidthsur la fenêtre. - Ajout rapide inutilisable : il manquait les destinations Facture, Devis et Client.
- Un test de cohérence mal conçu exigeait la présence d'un placeholder : il échouait dès que l'utilisateur le remplaçait, c'est-à-dire dès qu'il faisait ce qu'on lui demandait. Une vérification doit constater un état correct, pas figer un état transitoire.
- Une documentation non vérifiée dérive. CONTEXTE.md annonçait des compteurs
faux quelques heures après sa rédaction. D'où la suite
coherence-documentation.mjsqui vérifie automatiquement ses affirmations.
src/
main/ process principal (Node) — seul à toucher disque et base
index.ts fenêtre, enregistrement des 22 modules IPC
pdf.ts génération PDF (facture, devis, rappel)
maj.ts mises à jour (GitHub ou URL), désactivées par défaut
db/
schema.sql SOURCE DE VÉRITÉ du schéma
migrations.ts ajout de colonnes + retrait de contraintes, sans perte
database.ts ouverture, PRAGMA, transactions, checkpoint WAL
backup.ts sauvegardes locales horodatées + rotation
sauvegardeExterne.ts chiffrement AES-256-GCM (scrypt)
audit.ts traçage + verrou des exercices clôturés
migration-dossier.ts reprise de l'ancien dossier de données
ipc/ un fichier par domaine (21 fichiers)
preload/index.ts pont sécurisé — seule porte entre interface et système
renderer/src/
App.tsx menu, navigation, thème, langue, Ctrl+K, écran de conditions
pages/ 17 écrans
components/ 10 composants (Modale, Camembert, BarresAnnuelles,
LogoOhmnia, ConditionsUtilisation, RechercheGlobale,
ConnexionServeur, ReglageMultipostes…)
lib/ theme.ts, devise.ts, suggestions.ts
shared/ partagé entre les trois couches
types.ts tous les types
calculs.ts TOUTES les formules métier
pays.ts profils CH / FR / BE / LU / DE
i18n.ts traductions FR/EN
conditions.ts conditions d'utilisation de l'app + version
tests/ 19 suites — `npm run verifier`
Les compteurs ci-dessus doivent rester exacts : npm test vérifie que tous les
modules IPC sont enregistrés au démarrage et affiche leur nombre.
- Une formule = un seul endroit :
src/shared/calculs.ts. Toute division passe pardiviserSansErreur(). - Ajouter une colonne : l'écrire dans
schema.sqlET dansCOLONNES_ATTENDUESdemigrations.ts. Ne jamais supprimer la base pour appliquer un changement — l'utilisateur a de vraies données. - Opération multi-tables →
dansUneTransaction(). - Copier le fichier de base →
viderJournalWal()d'abord. - Nouveau canal IPC → déclarer dans
src/main/ipc/*.ts(ipcMain.handle) et danssrc/preload/index.ts(ipcRenderer.invoke).npm testvérifie l'appariement. - Pas de
window.prompt()→ composantModale. - Le PDF ne contient aucune information interne (notes de travail, case « à imprimer », marges). Thème clair, contrairement à l'interface.
- Vérifier avant de livrer :
npm run verifier.
Accueil (tableau de bord) · Ajout rapide (8 destinations) · Clients (fiche + factures) · Facturation (+ duplication, rappels, modèles) · Devis (+ conversion en facture) · Suivi du temps (chrono → lignes de facture) · Journal (+ justificatifs photo, graphiques) · Inventaire (alertes de seuil, décrément auto) · Modèles de prestations · Tarifs & Marge · Charges & Marge · Résumé annuel · Import/Export comptable (CSV + CAMT.053) · Audit & clôtures d'exercice · Mon entreprise (pays, TVA, CGV) · Paramètres de l'app (thème, langue, sauvegardes, mises à jour)
- Profils pays dans
src/shared/pays.ts: CH, FR, BE, LU, DE. Chacun porte devise, taux de taxe, format d'identifiant fiscal, mention de non-assujettissement, seuil indicatif, durée de conservation. - Ces valeurs sont des points de départ à vérifier, pas une certification. Les taux et surtout les seuils changent. C'est écrit dans le fichier et dans l'interface.
- Case « assujetti à la TVA » : si décochée, aucun montant de taxe n'est facturé et la mention légale du pays est imprimée. Le taux est forcé à 0.
- Conditions générales modifiables, avec un modèle de départ. L'utilisateur a été averti qu'une clause ne peut pas exclure la responsabilité en cas de faute grave ou intentionnelle (art. 100 CO en Suisse) et qu'un juriste doit relire.
- Panneau de conformité (module Audit) : contrôle les mentions obligatoires, la continuité et l'unicité de la numérotation. Il ne certifie rien.
src/shared/conditions.ts est monolingue, et c'est délibéré. Les quatre
autres applications de la maison ont leurs conditions dans les deux langues ;
Ohmnia non, et il ne faut pas « corriger » cela. Deux textes également
contraignants seraient pires qu'un seul : le jour où les deux versions ne
disent pas exactement la même chose, personne ne sait laquelle engage.
La page anglaise docs/en/terms.html est une traduction qui ne prévaut pas,
et elle l'annonce — tests/coherence-site.mjs exige d'ailleurs qu'elle contienne
le mot authoritative.
Ce qui manquait était ailleurs, et c'est le point qui compte : le site le disait, l'application ne le disait pas. Or c'est dans l'application qu'on accepte. Un anglophone y voyait une interface anglaise, un texte juridique français, et aucune explication — il acceptait un document qu'il ne pouvait pas lire, sans savoir qu'une traduction existait ni qu'elle ne prévalait pas.
L'écran d'acceptation porte maintenant la phrase, dans les deux langues, et
tests/coherence-site.mjs refuse qu'elle disparaisse. Un premier jet la
réservait à l'anglais avec une version française vide : tests/traductions.mjs
l'a refusée à juste titre — une chaîne vide sort à l'écran sans qu'on la voie.
Et elle a sa valeur en français aussi, puisqu'elle dit lequel des deux textes
prévaut.
Trouvé en vérifiant une hypothèse fausse. Je croyais que conditions.ts
aurait dû être bilingue comme ailleurs. Il ne devait pas l'être — mais aller
vérifier a montré le vrai manque, à côté de celui que je cherchais.
| Conditions de vente | Conditions d'utilisation | |
|---|---|---|
| Fichier | saisies par l'utilisateur en base | src/shared/conditions.ts |
| Entre qui | l'utilisateur et ses clients | l'éditeur d'Ohmnia et l'utilisateur |
| Où | Mon entreprise → imprimées sur factures | Écran au premier lancement |
| Sujet | garantie, paiement, réserve de propriété | l'app ne rend pas conforme, pas de garantie |
L'écran d'acceptation bloque l'application tant qu'il n'est pas validé. La case ne
s'active qu'après défilement complet du texte. Incrémenter VERSION_CONDITIONS
à chaque modification du texte : l'écran réapparaît alors pour relecture.
URL_CONDITIONS pointe vers https://resonlab.github.io/ohmnia/conditions.html.
La suite tests/coherence-site.mjs compare le texte de la page publique à ce fichier :
modifier l'un sans l'autre fait échouer npm run verifier.
Fait : infrastructure src/shared/i18n.ts, sélecteur de langue, navigation
complète, documents PDF entièrement traduits (testé FR et EN), et l'écran
Accueil.
t() sait maintenant insérer des valeurs : t('accueil.dansJours', { jours: 4 }).
Sans cela, une phrase citant un chiffre devait être découpée en morceaux à
concaténer — « Dans » + n + « j » — ce qui ne se traduit pas : l'ordre des mots
change d'une langue à l'autre.
Un détail qui trahissait la traduction à moitié : nomDuMois() écrivait
fr-CH en dur. Un anglophone lisait « août 2026 » au milieu d'un écran anglais.
La locale suit désormais la langue de l'interface — chercher les autres
toLocaleDateString en traduisant les écrans restants.
Au 13 août 2026 : 17 écrans sur 18 et 8 composants sur 10, 578 clés. Tous sauf « Mon entreprise » ; les deux composants restants ne portent aucun texte.
Trois choses trouvées en traduisant, et aucune n'était visible en lisant.
1. Deux écrans déclarés traduits ne l'étaient pas. Le contrôle du texte en
dur ne regardait que le texte entre balises — > Bonjour <. Il ne pouvait
donc pas voir un placeholder=, un alert('Client créé.'), un title=, ni un
ternaire {paye ? 'Payée' : 'En attente'}, c'est-à-dire une bonne part de ce
qu'un utilisateur lit. Trois messages français en dur dormaient dans
Clients.tsx et la liste des catégories dans Inventaire.tsx, sous une suite
verte.
Ce n'était pas un contrôle incapable d'échouer : il échouait très bien sur ce qu'il regardait. C'est la troisième forme, plus sournoise — une vérification dont on a étendu la confiance au-delà de son champ. Il refuse maintenant toute chaîne accentuée, sans exception de commodité : une exception serait la porte par laquelle il recommencerait à ne plus rien regarder.
Et il a produit un faux échec au premier essai : {/* ouvre un commentaire
JSX, que le détecteur ne reconnaissait pas, si bien que les apostrophes du
commentaire formaient de fausses chaînes. Corrigé avant d'être cru — un faux
échec use un contrôle aussi sûrement qu'un faux succès.
2. La langue enregistrée n'était jamais appliquée au démarrage. Signalé par
l'utilisateur, qui n'arrivait pas à changer de langue. L'effet qui lit les
paramètres est gardé par if (multipostes === null) return — la valeur au
montage, toujours — avec une liste de dépendances vide. Il sortait aussitôt
et ne repassait jamais. L'effet voisin, celui de la devise, porte la bonne
liste : c'est la comparaison des deux qui l'a montré.
Aucune suite ne le voyait. tests/effets-react.mjs a donc été écrite avant
le correctif : un useEffect qui sort sur une valeur doit la déclarer en
dépendance. Volontairement étroite — elle ne remplace pas
react-hooks/exhaustive-deps, et ajouter ESLint ici coûterait plus qu'il ne
rapporte.
3. Deux détails qui trahissent une traduction à moitié, de la même famille
que toLocaleDateString('fr-CH') en dur :
· la locale des dates vivait à quatre endroits, dont trois en dur. Elle est
maintenant dans locale(), dans i18n.ts ;
· le document imprimé était entièrement traduit sauf l'ordinal : un anglophone
recevait un « 2e reminder ». ordinal() suit la langue.
Ce qui est enregistré en base ne se traduit jamais — seul le libellé se
traduit. Les catégories d'inventaire (src/shared/inventaire.ts) et les actions
du journal d'audit portent une valeur canonique invariable et une clé
d'affichage. Traduire la valeur répartirait les mêmes articles dans deux
catégories selon la langue du jour où on les a saisis, et rendrait un journal
d'audit illisible à qui change de langue.
Reste à faire : « Mon entreprise » seulement.
Les composants sont inscrits dans la liste protégée, séparément des écrans. Le compte affiché les distingue — il a annoncé « 25 écrans sur 18 » le jour où ils l'ont rejointe, et un compte absurde décrédibilise une sortie aussi sûrement qu'un faux échec : on cesse de la lire, et le jour où elle dit quelque chose de vrai, personne ne le voit.
Un quatrième trou dans le contrôle du texte en dur, trouvé en traduisant les
derniers écrans : le relevé des chaînes littérales ne connaissait que
l'apostrophe et le guillemet, pas l'accent grave. Or un gabarit
`${nb} écritures ajoutées` est du texte affiché autant qu'une chaîne
ordinaire. Six messages y dormaient — dont deux dans SuiviTemps.tsx et
Facturation.tsx, deux écrans que je venais de déclarer traduits.
C'est la quatrième passe sur une seule question — « ce texte est-il traduit ? » — et chacune semblait complète. Quand un contrôle trouve un défaut, se demander ce qu'il ne regarde toujours pas.
Mon entreprise demande une décision avant d'être traduite, et elle n'est pas
technique. L'écran tire ses libellés de src/shared/pays.ts — nom de la taxe,
libellé de l'identifiant fiscal, seuil, mention de non-assujettissement. Or ces
chaînes ne sont pas toutes de la même nature :
· le nom de la taxe, le libellé de l'identifiant et les aides sont du texte d'écran : ils doivent suivre la langue de l'interface ; · la mention de non-assujettissement est une mention légale imprimée sur la facture. Elle doit rester dans la langue du pays d'émission, quelle que soit la langue de l'interface. La traduire parce que l'écran est en anglais produirait une facture française portant une mention en anglais.
Le modèle de conditions générales pose la même question : c'est un texte juridique qu'un juriste doit relire, et le traduire n'est pas une opération mécanique.
Ne pas expédier cet écran. Il vaut mieux qu'il reste français et honnête que traduit à moitié et faux — une facture qui porte une mention légale dans la mauvaise langue est un défaut qui se découvre chez le client, pas ici.
Un point laissé tel quel, et il est délibéré : domaines/documents.ts
fabrique la désignation « Frais de rappel (2e rappel) » avec l'ordinal français.
C'est une donnée écrite en base au moment de la création, pas du texte
d'interface : la retraduire après coup changerait une ligne de facture déjà
émise. À traiter avec les messages du processus principal, qui doivent devenir
des clés.
tests/traductions.mjs mesure l'avancement et l'affiche à chaque exécution.
Il vérifie trois choses : aucune clé sans version anglaise ni française
(une chaîne vide sortirait à l'écran, invisible) ; aucune clé déclarée sans être
employée ; et aucun texte français en dur dans un écran déclaré traduit.
La liste ECRANS_TRADUITS est ce qui rend le contrôle honnête sur un chantier à
moitié fait : elle dit ce qui est protégé, et le compte affiché dit ce qui reste.
Traduire un écran, c'est le traduire entièrement puis l'inscrire dans cette
liste — l'inverse fait échouer la suite, ce qui est le but.
Dix-sept clés ont été retirées au passage : déclarées, jamais employées. Une clé morte laisse croire qu'un écran est traduit alors qu'il ne l'est pas. Elles reviendront avec l'écran qui s'en servira.
Les trois contrôles ont été éprouvés en les cassant.
Méthode, écran par écran :
- Ajouter les chaînes dans
TEXTESdesrc/shared/i18n.ts(clé préfixée par l'écran, ex.client.nouveau). - Dans le composant :
import { t } from '../../../shared/i18n', puis remplacerNouveau clientpar{t('client.nouveau')}. npm run typecheck— les clés inconnues sont rejetées à la compilation.
Les messages d'erreur du main process (src/main/ipc/*.ts) sont aussi en français ;
ils devront recevoir le même traitement, ou être renvoyés sous forme de clé.
Les rappels existaient, mais il fallait y penser. Ouvrir la Facturation, parcourir l'historique, se souvenir de qui avait reçu quoi et quand. Une fonction dont il faut se souvenir n'est pas une fonction, c'est une intention.
Une carte en tête de la Facturation liste ce qui devrait partir aujourd'hui. Le calcul n'a rien de neuf — c'est celui qu'on faisait à la main — mais il est fait pour l'utilisateur au lieu d'être attendu de lui.
calculerRelances() vit dans src/shared/calculs.ts, avec les autres formules,
et tests/relances.mjs l'éprouve sans base ni fenêtre. Le SQL ne décide
rien : il rassemble statut, échéance, nombre de rappels et date du dernier ;
la règle tranche. Recopiée en SQL, elle finirait par contredire les tests qui la
vérifient — et tests/relances.mjs refuse d'ailleurs que l'écran recalcule un
retard de son côté.
Trois décisions, et chacune se défend :
- le seuil de première relance est celui des paramètres, pas un second réglage. Demander deux valeurs, c'est demander de tenir deux réglages cohérents entre eux — ce que personne ne fait ;
- un délai de 14 jours entre deux relances. Relancer un client deux jours de suite ne fait pas payer plus vite, ça fâche ;
- au-delà de trois rappels, on cesse de proposer. Un quatrième ne devient pas efficace ; c'est le moment d'un appel, d'une mise en demeure ou d'un abandon de créance. La facture reste affichée — elle est le vrai problème — mais sans bouton : proposer ce qui ne marche pas est pire que de ne rien proposer.
Rien n'est envoyé automatiquement. C'est une liste, pas un automatisme : un logiciel qui écrit tout seul à un client au nom de quelqu'un est un logiciel qu'on n'ose plus laisser tourner. Le nom « relances automatiques » décrivait mal ce qu'il fallait faire.
Quinze cas, six sabotages, six échecs. Les cas sont choisis pour discriminer : une facture payée avec 300 jours de retard, une facture relancée hier mais très en retard, et la borne exacte du délai — chacun échouerait sous la règle inverse.
rappels:supprimer est branché. journal:modifier reste fermé. Les deux
opérations attendaient une décision ; elles sont prises.
Le motif qui retenait rappels:supprimer était faux, et il a suffi d'aller
regarder pour le savoir. Il disait que la suppression laissait les frais sur la
facture. Les frais ne sont jamais écrits dans facture_lignes : ils vivent
sur la ligne rappels et ne deviennent une ligne de document que dans
construireDonneesRappel, qui les relit par l'id du rappel. Effacer la ligne
les efface partout. Une réserve écrite n'est pas une mesure : celle-ci a
survécu à plusieurs relectures et n'a coûté qu'un grep à réfuter.
Ce qui était vrai, en revanche : la création était tracée au journal d'audit et la suppression ne l'était pas. Le journal montrait des rappels émis et jamais aucun annulé — un journal d'audit incomplet est pire qu'absent, parce qu'on le lit comme s'il était complet.
journal:modifier a été retiré de bout en bout — domaine, IPC, pont,
registre du serveur et table des droits. Le retirer du seul écran n'aurait pas
suffi : une opération publiée au registre est appelable par le réseau en mode
multi-postes, sans passer par le moindre écran. Une porte condamnée côté salon
et laissée ouverte côté cave.
La liste de tests/atteignable.mjs est donc vide, et elle s'appelle
HORS_DATTEINTE_ASSUME et non plus EN_ATTENTE_DE_DECISION : garder l'ancien
nom aurait annoncé du travail en suspens là où il n'y en a plus.
Le retrait a été éprouvé dans les deux sens, parce qu'un retrait incomplet est le vrai risque ici : un droit laissé pour une opération disparue fait échouer « aucun droit ne porte sur une opération qui n'existe plus » ; une opération laissée au registre fait échouer « les opérations du serveur existent toutes comme canal IPC » et « ont toutes un droit déclaré ». Les trois garde-fous mordent.
Le code de la fonction est dans l'historique git si la décision change. Il était correct — il refusait un exercice clôturé, et refusait même qu'on en sorte une écriture en changeant sa date. Ce n'est pas sa qualité qui était en cause.
relancesAFaire() visait factures.date_echeance, une colonne qui n'a jamais
existé — ni dans schema.sql, ni dans les migrations, nulle part ailleurs que
dans ces trois lignes. Toute base la refusait — vérifié sur une base neuve et
sur la base réelle de l'utilisateur.
Et la panne était silencieuse. rechargerHistorique() appelle
factures.historique() avant rappels.aFaire() : l'historique se chargeait
normalement, seule la seconde promesse était rejetée, et la liste des relances
restait vide. L'écran n'affichait donc aucune erreur — il affichait « Rien à
relancer aujourd'hui », en permanence, quel que soit le nombre de factures en
retard. Un écran cassé se remarque ; un écran qui rassure à tort ne se remarque
jamais. La fonction phare du 13 août n'a jamais pu servir, et rien ne le disait.
Une première version de cette page annonçait « tout l'écran restait vide ». C'était faux, et c'est le lancement de l'application installée qui l'a montré : la Facturation s'ouvre parfaitement.
Partout ailleurs l'échéance se calcule — calculerEcheance(date, delai_paiement_jours), dans documents.ts comme dans tableauDeBord.ts.
Cette requête-là avait inventé une colonne au lieu d'appeler la formule.
Aucune suite ne pouvait le voir, et c'est le point à retenir.
tests/relances.mjs éprouve calculerRelances sans base — c'est sa qualité, et
il faut la garder. tests/atteignable.mjs constate qu'un écran appelle
rappels.aFaire, pas que l'appel aboutisse ; c'est écrit en tête de ce
fichier depuis le début. Il manquait une suite qui exécute la requête.
tests/rappels-annulation.mjs le fait, sur une vraie base, en compilant les
vrais domaines avec esbuild — même procédé que
serveur-authentification.mjs, et pour la même raison : transcrire les requêtes
dans le test ne prouverait que la justesse de la transcription. Quatre
sabotages, quatre échecs.
La leçon complète la famille des pannes déjà connues ici. On savait qu'un mécanisme peut exister sans être atteignable. Celui-ci était atteignable, branché, sous une suite verte — et mort à l'exécution. Une règle éprouvée hors base et une requête jamais exécutée donnent ensemble l'apparence d'une fonctionnalité vérifiée.
- QR-facture suisse (Swiss QR-bill) — écartée par l'utilisateur, pas encore assujetti.
- Décompte TVA trimestriel — écarté pour la même raison.
- Ces deux points redeviendront pertinents dès qu'il dépassera le seuil.
Fait : dépôt public github.com/ResonLab/ohmnia,
branche main poussée. Le dépôt s'appelait Leimmingz/ohmnia ; il a été
transféré dans l'organisation ResonLab le 10 août 2026, avec les quatre
autres dépôts de la maison. GitHub redirige l'ancienne adresse du dépôt, mais
pas celle du site : GitHub Pages sert désormais resonlab.github.io/ohmnia.
Toutes les adresses du code, des pages et de la documentation ont été réécrites,
URL_CONDITIONS comprise, ainsi que la liste blanche de tests/coherence-site.mjs
qui nommait l'ancien compte.
gh (GitHub CLI) est installé et authentifié sur le compte Leimmingz,
administrateur de l'organisation.
Fait : les deux branches sont fusionnées dans main —
securite-et-documentation, et serveur-multipostes le 10 août 2026 (voir §12).
Fait : le site est en ligne sur https://resonlab.github.io/ohmnia/ (GitHub Pages,
branche main, dossier /docs). Douze pages, six en français et six en anglais
dans docs/en/. Licence MIT ajoutée.
Reste à faire : aucune release publiée. Marche à suivre dans
SITE-GITHUB.md : jeton d'accès → npm run publish:win.
La cible Linux (AppImage et .deb) est configurée mais jamais construite :
electron-builder ne sait pas produire un paquet Linux depuis Windows, il faut
WSL ou GitHub Actions.
Tant qu'aucune release n'existe, l'auto-updater n'a rien à trouver.
Le site disait ce que fait l'application et ce qu'elle ne fait pas. Il ne disait nulle part par où commencer. Quelqu'un qui télécharge se retrouve devant une application vide sans savoir quoi cliquer, et c'est là qu'on perd les gens — pas à la page d'accueil.
src/shared/guide.ts porte le texte dans les deux langues et nulle part ailleurs :
scripts/publier-guide.mjs en déduit docs/guide.html et docs/en/guide.html,
scripts/guide-pdf.mjs en tire les deux PDF joints aux releases. Un guide
recopié à la main divergerait au premier correctif — et c'est le document qu'on
emporte, donc celui qu'on croit.
L'ordre des étapes n'est pas décoratif : c'est celui dans lequel
l'application ne refuse rien. tests/coherence-guide.mjs le vérifie, en plus de
refuser qu'une page diverge de la source, qu'une traduction soit vide, ou qu'une
étape perde son piège. Les pièges sont la moitié de la valeur : ce sont les
choses qu'on ne devine pas et qui coûtent une soirée.
npm run guide:publier # les deux pages
npm run guide:pdf # les deux PDF, dans release/Trois défauts de ce mécanisme, trouvés en le portant d'une application à l'autre, et corrigés dans les quatre dépôts :
· un seuil de longueur prenait « Receipts » et « Backups » — des titres anglais
parfaitement traduits — pour des traductions vides. On teste désormais le
vide, pas la longueur. Un faux échec use un contrôle aussi sûrement qu'un
faux succès ;
· le caractère & s'écrit & en HTML : le contrôle annonçait un texte
disparu alors que la page était juste ;
· une liste figée d'ancres à réécrire laissait des ancres mortes sur le guide,
les sections d'une page d'accueil ne portant pas les mêmes noms d'une
application à l'autre. Toutes les ancres renvoient maintenant à l'accueil.
Le PDF a révélé un bug qui traînait dans la maison depuis des semaines :
« fabriquer-icones.mjs échoue au-delà de la première image ». Ce n'est ni le
chemin ni le fichier temporaire — créer une seconde BrowserWindow après
avoir travaillé dans la première fait échouer son chargement sur ERR_FAILED.
Une seule fenêtre réutilisée, et les deux PDF sortent. Une hypothèse a été
suivie puis abandonnée, et elle est notée dans le code : loadFile produit bien
sous Windows une adresse mêlant file:/// et des antislashs. C'est vrai, c'est
corrigé, et ça n'a rien changé.
npm run verifier: typecheck + 19 suites de tests, tout passe.- Version
0.1.3. Construite par GitHub Actions pour Windows et Linux. Elle apporte le mode multi-postes, le passage à l'organisation ResonLab et les premiers écrans traduits. La0.1.2n'a jamais été publiée : son étiquette pointe sur du code antérieur à la fusion. Corrige deux defauts : les boites de dialogue natives sans fenetre parente, qui pouvaient passer derriere l'app en gardant le focus clavier, et la verification de mise a jour qui restait figee sans message. L'ancienne release0.1.0, televersee a la main, a une auto-mise a jour cassee : sonlatest.ymlannonceOhmnia-Setup-0.1.0.exealors que le fichier en ligne s'appelleOhmnia.Setup.0.1.0.exe(404). A supprimer. Ancien installateur local :release\Ohmnia Setup 0.1.0.exe(96 Mo) aveclatest.ymlpour l'auto-updater. - Base de l'utilisateur :
%APPDATA%\Ohmnia\gestion.sqlite, 25 tables, intégritéok. - Les données de test créées pendant le développement ont été supprimées. L'entreprise « Valclair » est configurée, pays Suisse, non assujettie à la TVA.
- Le logo de l'entreprise n'est pas configuré : les PDF sortent sans logo tant que l'utilisateur n'en a pas choisi un dans « Mon entreprise ».
- Les conditions d'utilisation n'ont pas encore été acceptées : l'écran s'affichera au prochain lancement.
| Fichier | Valeur | État |
|---|---|---|
electron-builder.yml |
owner |
✔ renseigné : ResonLab |
src/shared/conditions.ts |
URL_CONDITIONS |
✔ bascule sur l'adresse GitHub Pages faite |
Organisation GitHub du projet : ResonLab, dépôt ohmnia.
npm test rappelle ce qui reste à remplacer sans faire échouer la vérification.
Un jeton d'accès GitHub a été exposé en capture d'écran pendant le développement. Il a été signalé et doit avoir été révoqué. Si tu vois un secret dans une capture ou un message, signale-le immédiatement et en premier, avant toute autre réponse.
Rappels appliqués au projet :
.gitignoreexclut.envet.claude/- Aucun secret n'est stocké dans le code
- Le mot de passe des sauvegardes chiffrées n'est enregistré nulle part (par choix)
L'utilisateur travaille souvent en autonomie déléguée (« débrouille-toi »). Il attend :
- qu'on teste réellement ce qu'on livre, pas qu'on affirme que ça marche ;
- qu'on signale honnêtement ce qui n'a pas pu être vérifié ;
- qu'on corrige les bugs qu'il signale en cherchant la cause, pas en contournant.
Le serveur s'appelle Nexika et vit hors de ce projet, parce qu'il sert aussi Scenika. Tout ce qui suit parle de lui.
La branche
serveur-multipostesest fusionnée depuis le 10 août 2026. Elle attendait une seule chose : que Nexika cesse d'être un chemin relatif. Les trois imports desrc/serveur/visaient../../../Nexika/serveur/index, si bien qu'un clone du seul dépôtohmniane compilait plus son serveur. Nexika est maintenant le paquetgithub:ResonLab/nexika, et l'on écritfrom 'nexika'.Vérifié en clonant pour de vrai, dans un dossier où
Nexika/n'existe pas :npm install,npm run typecheck,npm run serveur:build, le serveur qui répond à--aide, et la suite complète. C'est le seul contrôle qui prouve quelque chose ici — le projet de développement, lui, a toujours le dossier voisin sous la main et ne peut pas voir le problème.
npm 12 refuse par défaut toute dépendance installée depuis un dépôt git
(allow-git = none), et c'est une bonne protection de la chaîne
d'approvisionnement. .npmrc l'ouvre avec root, pas all : les
dépendances git déclarées par ce projet sont autorisées, celles qu'une
dépendance amènerait dans son dos ne le sont pas. Sans ce fichier,
npm install échoue sur EALLOWGIT.
Modifier Nexika ne suffit pas à le voir ici. npm garde la copie clonée dans
node_modules : il faut repousser Nexika puis npm install github:ResonLab/nexika
à nouveau, ou passer par npm link pour travailler sur les deux à la fois.
src/main/contexte.ts: où vivent les données et quelle version tourne. La couche Electron le renseigne au démarrage. La couche base de données n'importe plus Electron du tout.src/main/domaines/: la logique métier, sans Electron. Les 20 domaines sont convertis — la conversion module par module est terminée.src/main/ipc/: ne fait plus que brancher les canaux sur la fenêtre. Il n'y reste du SQL que dans les trois endroits listés plus bas, où la requête ne fait qu'enregistrer le résultat d'une boîte de dialogue.src/serveur/: squelette de serveur HTTP qui réutilisedomaines/. Protocole :POST /api/<canal>avec{ "arguments": [...] }. Les noms de canaux sont exactement ceux de l'IPC, pour que les deux modes ne puissent pas diverger. Le nombre d'opérations exposées n'est pas écrit ici : il disait 99 le 16 août 2026 alors que le registre en portait 105, et personne n'était chargé de le mettre à jour quand il change. C'est la règle déjà appliquée aux numéros de version dans les badges du site.tests/serveur-multipostes.mjsl'affiche à chaque exécution, à côté du contrôle qui apparie registre et IPC — donc là où il ne peut pas mentir.tests/serveur-multipostes.mjs: démarre un vrai serveur sur une base temporaire et fait de vrais appels réseau.
Étape 2 faite : comptes, droits, authentification.
src/serveur/comptes.ts: comptes, mots de passe, sessions, journal des accès. Base séparéecomptes.sqlite, pas dansgestion.sqlite— les comptes seront communs à Ohmnia et Scenika, et le mode local ne doit pas hériter d'un schéma dont il n'a pas l'usage.src/serveur/droits.ts: qui a le droit de quoi, dans un seul fichier, opération par opération. Trois rôles :lecture<ecriture<administration.src/serveur/index.ts: toute opération métier exige une session. Le jeton voyage dansAuthorization: Bearer <jeton>.tests/serveur-authentification.mjs: compile le vrai serveur avec esbuild et l'attaque par le réseau. Rien n'y est transcrit — une authentification réécrite pour le test ne prouverait que la justesse de la réécriture.
Protocole d'authentification :
| Appel | Effet |
|---|---|
serveur:etat |
public — dit si un compte existe déjà |
comptes:creerPremierAdministrateur |
public, mais refusé dès qu'un compte existe |
session:ouvrir |
public — rend un jeton valable 12 h |
| tout le reste | jeton obligatoire, rôle vérifié |
Codes de réponse : 400 erreur métier · 401 pas de session · 403 droits
insuffisants · 404 opération inconnue.
- Aucun import d'Electron dans
domaines/niserveur/. - Pas de réseau sans administrateur. Le serveur refuse d'écouter ailleurs
que sur
127.0.0.1tant qu'aucun compte administrateur n'existe : sinon le premier venu créerait le sien et prendrait la comptabilité. - Chaque opération du registre doit exister comme canal IPC. Vérifié automatiquement : une faute de frappe fait échouer la suite.
- Chaque opération du registre doit avoir un droit déclaré. Une opération
sans droit est refusée à l'exécution, et
npm testla signale. Les droits ne sont jamais déduits du nom du canal : une règle «lister= lecture » se tromperait en silence surconditions:accepterourecherche:globale. - Le message d'échec de connexion est le même que le compte existe ou non. Sinon la page de connexion devient un moyen de savoir qui travaille ici.
- Le dernier administrateur ne peut être ni rétrogradé ni désactivé. Un serveur sans administrateur ne se reprend plus en main.
Tous ont été vérifiés en les cassant volontairement.
Le transport n'est pas chiffré. Mots de passe et jetons circulent en clair
en HTTP. Sur le réseau local d'une petite entreprise, c'est un risque assumé ;
dès que le serveur est joignable au-delà, il faut le mettre derrière un
reverse-proxy HTTPS. C'est écrit en tête de src/serveur/index.ts pour que
personne ne le découvre en production.
Cinq modules gardent une part Electron : comptabilite, conditions, entreprise, justificatifs, parametresApp. Ils ouvrent des sélecteurs de fichiers ou l'explorateur, ce qui n'a aucun sens sur un serveur. Ce qui reste là-bas n'est jamais du métier :
| Module | Ce qui reste côté fenêtre |
|---|---|
| comptabilite | choisir le fichier de relevé à lire, choisir où écrire l'export CSV |
| conditions | ouvrir la page des conditions dans le navigateur du poste |
| entreprise | choisir le logo, le copier, le lire en data URL |
| justificatifs | ajouter, ouvrir, lire, supprimer les fichiers sur le disque |
| parametresApp | dossiers, sauvegardes, infos système, export de toutes les données |
Les justificatifs sont le seul point vraiment en suspens : leur lecture et leur suppression visent le disque du poste. En mode serveur, ces fichiers devront vivre à côté de la base du serveur. C'est une décision de l'étape 3, pas d'un simple déplacement de code.
Le mode se choisit dans « Paramètres de l'app » → Mode multi-postes. Le mode local reste le défaut : sans choix explicite, rien ne change.
src/main/multipostes/configuration.ts:multipostes.jsondans le dossier de données. Jamais le mot de passe — il est redemandé à chaque ouverture.src/main/multipostes/client.ts: le poste vu comme client. Le jeton ne vit qu'en mémoire : écrit sur le disque, il survivrait au vol du portable.src/main/multipostes/routeur.ts: le seul endroit qui décide, base locale ou serveur.src/main/multipostes/handlers.ts: en mode serveur, les canaux métier sont enregistrés depuisDROITSet renvoyés au serveur. Mêmes noms qu'en IPC, donc l'interface n'a rien à savoir du mode.
Le mode est décidé au démarrage, une fois. En mode serveur, la base locale n'est même pas ouverte — c'est ce qui garantit qu'aucun écran n'affiche par erreur des données locales. Changer de mode recharge l'application ; les données locales ne sont jamais effacées.
Trois choses qui auraient sorti des écrans vides et qu'il a fallu redécouper :
| Ce qui casse naïvement | Découpage retenu |
|---|---|
| Les PDF lisaient la base en direct | documents:donnees rend le document tout prêt, logo compris en data URL ; pdf.ts ne fait plus que l'imprimer |
| L'export comptable construisait le CSV sur le poste | comptabilite:construireCsv le construit là où sont les données ; le poste choisit seulement où l'écrire |
| L'import bancaire rapprochait les écritures localement | comptabilite:analyserReleve rapproche là où vit le Journal |
Dans chaque module mixte, deux fonctions d'enregistrement au lieu d'une :
enregistrerHandlersX() pour les données (mode local seulement) et
enregistrerHandlersXPoste() pour ce qui concerne cette machine-ci (les deux
modes). C'est ce qui rend main/index.ts lisible d'un coup d'œil.
Ce qui est refusé en multi-postes, avec un message qui dit pourquoi : sauvegardes locales, export global, choix du logo, justificatifs. Ces opérations écriraient sur le poste alors que les données sont sur le serveur — un justificatif qu'on croit rangé et qui n'existe nulle part est pire que pas de justificatif.
Session expirée : le poste prévient l'interface (multipostes:sessionPerdue),
qui repasse par l'écran de connexion sans détruire l'écran en cours.
Rôle lecture : les modules Audit et Mon entreprise disparaissent du menu,
et un bandeau annonce la lecture seule.
Le serveur est commun à la maison. Tout ce qui ne dépend pas d'Ohmnia —
transport, comptes, sessions, droits, certificat, ligne de commande — vit dans
le paquet nexika, parce que Scenika s'en servira aussi. Ne restent dans
src/serveur/ que les canaux d'Ohmnia (registre.ts), leurs droits
(droits.ts) et le branchement (ohmnia.ts, quinze lignes).
L'application Electron, elle, ne dépend jamais de nexika : elle ne parle
au serveur que par le réseau. C'est ce qui évite que son installateur embarque
le code du serveur. Une vérification refuse tout import de nexika depuis
main/, renderer/ ou preload/ — sans elle, on ne s'en apercevrait qu'au
moment de publier.
Ce garde-fou lit la provenance de l'import, pas une chaîne de caractères, et
il faut que ça le reste. Il visait autrefois le chemin Nexika/serveur ; le
jour où Nexika est devenu un paquet, from 'nexika' serait passé dessous sans
rien déclencher. Le contrôle aurait continué de dire oui en ne regardant plus
rien — pire qu'un contrôle absent, parce qu'on lui fait confiance.
demarrerServeur n'était appelé que par les tests : la fonctionnalité
passait toutes les vérifications sans que personne puisse s'en servir. D'où
src/serveur/principal.ts et scripts/construire-serveur.mjs.
npm run serveur:build # produit out/serveur/ohmnia-serveur.mjs
node out/serveur/ohmnia-serveur.mjs --aideLe serveur est compilé en un seul fichier : on le copie sur la machine qui
héberge les données, sans npm install ni le reste du projet. Aucune
dépendance native — node:sqlite est intégré à Node 24.
node ohmnia-serveur.mjs --donnees "D:\OhmniaServeur" --port 8787Options : --donnees (obligatoire), --port, --hote, --certificat,
--cle, --aide. Une option mal orthographiée est signalée, pas ignorée.
VERSION_SERVEUR dans src/serveur/version.ts est écrite en dur — le fichier
compilé n'a pas de package.json à côté. npm test vérifie qu'elle n'a pas
dérivé de celle du projet.
- Démarrer le serveur sur
127.0.0.1(le défaut). - Depuis un poste : Paramètres de l'app → Mode multi-postes, renseigner l'adresse, puis créer le premier administrateur.
- Arrêter le serveur, le rouvrir avec
--hote 0.0.0.0et un certificat. - Créer les comptes des collègues depuis un poste administrateur.
Les deux garde-fous imposent cet ordre : pas de réseau sans administrateur, et pas de réseau sans chiffrement. Fabriquer un certificat auto-signé (OpenSSL est livré avec Git pour Windows) :
node ohmnia-serveur.mjs --creer-certificat --donnees "D:\OhmniaServeur"Le certificat est fabriqué par l'application, sans OpenSSL. Exiger d'aller
chercher un outil externe et d'y recopier une ligne ésotérique, c'est pousser
à laisser le serveur en clair — soit faire échouer la mesure de sécurité par sa
mise en œuvre. src/serveur/certificat.ts construit la structure X.509 en DER
à la main (Node génère des clés, pas des certificats). Il couvre localhost,
127.0.0.1 et les adresses IPv4 de la machine : sans elles, un poste qui
vise https://192.168.1.20:8787 ne pourrait rien vérifier.
Le serveur reprend certificat.pem et cle.pem du dossier de données sans
qu'on ait à les désigner. Un certificat existant n'est jamais écrasé : les
postes qui l'ont accepté redemanderaient tous confirmation.
Chaque poste devra l'accepter une première fois : c'est normal pour un certificat auto-signé. Il chiffre parfaitement la liaison ; ce qu'il ne fait pas, c'est prouver à un inconnu qu'il parle au bon serveur.
Les justificatifs vivent avec la base. Le fichier voyage en base64 par le
protocole et se range à côté de gestion.sqlite — donc sur le serveur en
multi-postes, et tous les postes le voient. Limite à 15 Mo par fichier, avec un
message qui explique quoi faire. Le corps maximal du protocole est passé à
25 Mo pour l'encodage base64. En local, le dossier ne change pas
(%APPDATA%\Ohmnia\Justificatifs) : rien à migrer.
Le logo est rangé dans la base, colonne entreprise.logo_donnees, en data
URL. Avant, seul son chemin était mémorisé : il disparaissait des documents dès
qu'on changeait de machine. Le chemin reste lu en secours pour les bases
d'avant ce changement. Limite à 2 Mo — il voyage dans chaque PDF.
Thème, langue et couleur d'accent sont propres au poste en multi-postes.
Ils vivent dans parametres_app avec des réglages d'entreprise réservés à
l'administration : les laisser côté serveur revenait à interdire à un employé
de choisir son thème. parametresApp:lire et :enregistrer sont donc les
deux seuls canaux du registre qui ne sont pas renvoyés au serveur — voir
CANAUX_NON_RENVOYES dans multipostes/handlers.ts. Une vérification impose
que toute exception y soit servie par le poste, sinon le canal n'existerait
plus du tout en mode serveur.
Les actions d'écriture disparaissent pour le rôle lecture. Chaque bouton
concerné porte action-ecriture ou action-administration ; deux attributs
sur .app et une règle CSS décident. Choix assumé : marquer soixante boutons
d'une classe est plus court et plus sûr à relire que soixante enveloppes JSX.
display: none plutôt qu'un bouton grisé — un bouton désactivé invite à
chercher comment l'activer. Le vrai garde-fou reste le serveur, qui refuse
l'opération de toute façon.
Le serveur sait parler HTTPS : options certificat et cleePrivee. Et
surtout, il refuse d'écouter sur le réseau sans chiffrement — les mots de
passe et les jetons y circuleraient en clair. Sur 127.0.0.1, HTTP reste
accepté : le trafic ne quitte pas la machine. Les deux garde-fous du réseau
(un administrateur, du chiffrement) sont vérifiés séparément.
- Le serveur n'est pas installé comme service Windows. Il faut le lancer à la main ou par une tâche planifiée. Il s'arrête proprement sur Ctrl+C (Windows envoie l'équivalent à l'arrêt d'une tâche), en refermant les bases.
- La traduction anglaise des nouveaux écrans (connexion, mode multi-postes) n'est pas faite — comme le reste des écrans, voir §8.
Des vérifications visaient ipc/, où le code n'est plus. Deux suites
lisaient le fichier source de ipc/ : audit-securite.mjs y cherchait
dansUneTransaction dans les factures et les devis, et parseurs-bancaires.mjs
y découpait les analyseurs de relevés pour les exécuter. Déplacer le code sans
déplacer le contrôle aurait laissé passer une écriture hors transaction en mode
réseau. Les deux pointent désormais sur domaines/. En convertissant un
module, chercher son nom dans tests/ avant de conclure.
parseurs-bancaires.mjs découpe domaines/comptabilite.ts entre deux
commentaires — « Découpe une ligne CSV » et « Marque les mouvements » — puis
retire les types pour exécuter le bloc. Ce bloc ne doit donc toucher ni la base
ni Electron, et ses fonctions doivent rester internes au module : le test
ajoute ses propres export.
src/serveur/ n'était pas typechecké. tsconfig.node.json ne listait que
main, preload et shared : une erreur de syntaxe dans le serveur passait
npm run typecheck sans broncher et n'apparaissait qu'à la compilation. Le
dossier est désormais inclus. En ajoutant un dossier source, l'ajouter aussi
au tsconfig.
Le nom du canal doit tenir sur la même ligne qu'ipcMain.handle(. Le
garde-fou qui apparie le registre et l'IPC cherche ipcMain.handle('<canal>'
d'un seul tenant. Une mise en forme sur plusieurs lignes rend le canal
invisible et fait échouer la suite, alors que le code est correct.
fetch garde des connexions dans un pool : Node plantait sur une assertion
interne en quittant, alors que tous les tests passaient. Le code de sortie
était faux. Dans les tests, utiliser le module node:http et ne jamais appeler
process.exit() pendant la fermeture des sockets.