← Tous les manuels · Portail

MANUAL - compta

Manuel d'instructions de cette app. À LIRE avant toute intervention, et à METTRE À JOUR après chaque modification (ajouter une ligne au CHANGELOG en bas).
Dernière génération/MAJ : 2026-09-03 · Statut : à jour (onglet Photo perso)

1. Identité

  • - Rôle : compta par photo. Les équipes terrain (une par entreprise) photographient leurs factures/reçus depuis un lien mobile /snap/:token ; extraction auto des champs (fournisseur, montant, date, devise, numéro, catégorie) par vision OpenAI (gpt-4o-mini), avec dégradation gracieuse en saisie manuelle si aucune clé. Le propriétaire consulte, valide et exporte le registre (CSV) depuis un dashboard protégé par mot de passe, avec deux onglets : Registre (toutes les factures, filtres, export) et Photo (capture directe des factures perso de Matt, sans lien/token). Les fonctionnalités multi-entreprises (gestion/ajout d'entreprise, liens de capture) sont masquées côté dashboard depuis le 2026-09-03 (flag SHOW_COMPANY_FEATURES dans public/index.html, voir §7), pas supprimées.
  • - URL publique : https://compta.panelbay.com (déployée le 2026-08-06, Cloudflare DNS + nginx + Let's Encrypt).
  • - Entité : usage interne des 3 entités de Matt (Resident Paraguay EAS, MOA Real Estate S.A., MOA Properties), seedées au premier démarrage.
  • 2. Exécution / infra

  • - Dossier : /root/workspace/apps/compta
  • - Stack : Node.js (ESM) + Express 4 + better-sqlite3 + multer. Deux frontends statiques dans public/ (capture.html mobile équipe, index.html dashboard propriétaire).
  • - Service systemd : compta.service (systemctl restart compta.service). Actif, Restart=always, écoute 127.0.0.1:8326.
  • - Port : 8326 (par défaut ; surchargé par PORT). Écoute sur 127.0.0.1.
  • - Reverse proxy (nginx) : compta.panelbay.com → 127.0.0.1:8326 (HTTPS). client_max_body_size 25m posé dans le vhost (sinon 413 sur les PDF/photos > 1 Mo, la limite nginx par défaut). app.set('trust proxy', true) pour l'IP réelle derrière nginx.
  • - Redémarrer : systemctl restart compta.service.
  • - Config : .env à la racine de l'app (chargé par env.js, importé en premier). Variables : COMPTA_SECRET, OWNER_PASSWORD, COMPTA_TOKEN_TTL_MS, COMPTA_AMOUNT_CAP, INVOICE_OCR_KEY/OPENAI_API_KEY, PORT, COMPTA_BASE_URL.
  • 3. Structure & fichiers clés

  • - server.js - serveur Express, routes API, auth propriétaire, upload.
  • - env.js - chargeur .env minimal (aucune dépendance), à importer AVANT ocr.js/db.js.
  • - db.js - schéma SQLite + seed des entreprises + migration file_hash.
  • - ocr.js - extraction vision OpenAI (dégradation propre si pas de clé).
  • - public/capture.html - page mobile équipe (compression client, skeleton).
  • - public/index.html - dashboard propriétaire (registre paginé, drawer, export).
  • - integrations/drive_sync.py - synchro Google Drive perso de Matt (voir §6).
  • - .env - config/secrets (valeurs jamais dans ce manuel).
  • 4. Données

  • - Base / stockage : data/compta.db (SQLite, WAL). Fichiers uploadés dans data/uploads/<capture_token>/.
  • - Modèle / tables :
  • 5. API / endpoints

  • - GET /api/companies (proprio) - ne renvoie que les entreprises hidden=0 (donc jamais Personnel (Matt)).
  • - GET /api/export.csv
  • - GET /api/file/:id
  • - GET /api/invoices
  • - GET /api/snap/:token
  • - GET /api/summary
  • - GET /healthz
  • - GET /snap/:token
  • - POST /api/companies
  • - POST /api/invoices/:id
  • - POST /api/invoices/:id/status
  • - POST /api/invoices/photo (proprio, ownerAuth) - upload direct depuis l'onglet Photo du dashboard (form-data photo, sans token/lien), rattaché à l'entreprise interne Personnel (Matt). Même pipeline que l'upload équipe (dédup par hash, OCR, sync Drive) via la fonction partagée saveInvoiceFile() dans server.js.
  • - POST /api/login
  • - POST /api/snap/:token/confirm/:id
  • - POST /api/snap/:token/confirm-batch (mode rafale : {uploader, items:[{id, ...champs}]}, une transaction pour toute la salve)
  • - POST /api/snap/:token/upload
  • 6. Intégrations & secrets

  • - Intégrations :
  • - Secrets / clés (OÙ, jamais la valeur) : fichier .env à la racine de l'app.
  • 7. Pièges connus / à savoir

  • - env.js doit rester le PREMIER import de server.js : ocr.js lit process.env à l'évaluation du module ; si .env est chargé après, la clé OCR n'est pas vue.
  • - Auth propriétaire : jeton = base64url({sub,exp}).HMAC, à expiration (COMPTA_TOKEN_TTL_MS, défaut 12h). Il n'est PLUS accepté en query string ?t= : fichiers et export CSV se récupèrent via header Authorization: Bearer (fetch + blob côté front). Ne pas réintroduire ?t=.
  • - Rate-limit login : en mémoire (Map par IP), remis à zéro au redémarrage. 5 échecs → backoff exponentiel (429). Derrière nginx, garder trust proxy pour une vraie IP.
  • - Idempotence upload : dédup par (company_id, file_hash SHA-256) sur 24h ; un renvoi renvoie la ligne existante (duplicate:true) sans ré-insérer. La compression client change les octets d'une image : une même photo re-photographiée n'est pas un doublon (attendu).
  • - Migration file_hash : db.js fait un ALTER TABLE ADD COLUMN idempotent (non destructif) au démarrage.
  • - Montants : validés serveur (fini, ≥ 0, ≤ COMPTA_AMOUNT_CAP) sur upload, confirm et update ; sinon 400.
  • - Drive sync sans date fiable : si l'OCR ne lit pas de date (photo illisible, saisie manuelle), le classement initial se fait sur la date d'UPLOAD, pas la date de facture. Si l'équipe/Matt corrige la date ensuite (confirm-batch, drawer dashboard), le fichier est automatiquement déplacé vers le bon mois (driveMaybeRefile) : pas besoin d'intervention manuelle sur Drive.
  • - drive_sync.py appelle python3 système (pas de venv dédié à compta) : googleapiclient/google-auth-oauthlib doivent rester installés globalement sur la machine (déjà le cas, partagés avec le reste de l'AIOS). Si ce module bouge un jour vers un venv, mettre à jour l'appel execFile('python3', ...) dans server.js.
  • - Entreprise masquée != entreprise supprimée : hidden=1 ne fait que filtrer GET /api/companies (donc le filtre et la liste de liens du dashboard) et le byCompany de /api/summary. Personnel (Matt) continue de fonctionner normalement en base (factures, dédup, Drive) ; elle apparaît telle quelle dans /api/invoices et l'onglet Registre (colonne Entreprise), volontairement (les factures perso doivent rester visibles/exportables dans le registre général).
  • - Front : les deux onglets restent dans le DOM en permanence (display:none sur celui d'arrière-plan, jamais détruit/reconstruit) : ça évite de casser loadTable()/refresh() qui référencent des éléments de l'onglet Registre (#tbl, .kpi .n) même quand l'onglet Photo est actif. Si un jour l'un des deux onglets est rendu conditionnellement (retiré du DOM), il faut garder ces fonctions défensives (if($('tbl'))...).
  • - Réactiver les fonctionnalités entreprises : repasser SHOW_COMPANY_FEATURES à true dans public/index.html (haut du <script>). Aucun autre changement nécessaire, le code (filtre, liste de liens, ajout d'entreprise) est resté intact, juste non rendu.
  • 8. CHANGELOG

  • - 2026-09-03 - Onglet Photo (factures perso de Matt) + masquage des fonctionnalités entreprises. (1) Deux onglets dans le dashboard propriétaire (public/index.html) : Registre (contenu inchangé : KPIs, filtres, table, tiroir de détail, export CSV) et Photo (nouveau : capture directe, sans lien/token, avec compression client + OCR + champs éditables + boutons Enregistrer/Valider par facture, même style que le mode rafale de capture.html mais authentifié ownerAuth). Les deux onglets restent dans le DOM, seule la visibilité bascule (showTab()), pour ne rien casser des fonctions existantes. (2) Backend : nouvelle entreprise interne masquée Personnel (Matt) (db.js, colonne companies.hidden ajoutée par migration non destructive, getOrCreatePersonalCompany() idempotent au démarrage) qui reçoit les factures de l'onglet Photo. Nouvel endpoint POST /api/invoices/photo (ownerAuth, form-data photo), logique d'upload factorisée dans saveInvoiceFile() (dédup hash, OCR, insertion, sync Drive) et réutilisée par l'ancienne route /api/snap/:token/upload sans changement de comportement. GET /api/companies et le byCompany de /api/summary filtrent désormais hidden=0. (3) Fonctionnalités entreprises masquées, pas supprimées : filtre "Toutes les entreprises" et section "Liens de capture par entreprise / Ajouter une entreprise" cachés derrière const SHOW_COMPANY_FEATURES = false en tête du <script> de index.html ; le code (HTML + handlers) reste intact, true les réaffiche. Voulu par Matt (2026-09-03) : ne gérer que ses factures perso pour le moment. Testé de bout en bout en local après restart : node --check OK sur server.js/db.js et sur le script extrait de index.html, /api/companies ne renvoie plus que les 3 entreprises (pas Personnel (Matt)), upload réel via /api/invoices/photo → OCR → facture visible dans le Registre avec company: "Personnel (Matt)" → sync Drive confirmée (drive_file_id renseigné) → ré-upload du même fichier → duplicate:true (dédup OK) ; reste une ligne de test (f_GriEGjUR8Q) en base et un fichier de test sur le Drive perso de Matt, à nettoyer manuellement (le hook de garde-fou bloque les DELETE FROM en Bash, même limitation que le 2026-08-31). Fichiers : db.js, server.js, public/index.html.
  • - 2026-08-31 - Synchro Google Drive perso (dossier "Facturas"). Chaque facture enregistrée par l'équipe part désormais aussi vers le Drive perso de Matt (matt.bouthors@gmail.com), dans Facturas/<Mois espagnol> <Année> (dossier qu'il utilisait déjà à la main, ex. Facturas/Agosto 2026 : réutilisé tel quel, pas de nouveau dossier créé). Classement sur la date de la facture (OCR), repli sur date d'upload si absente ; déplacement auto si la date est corrigée après coup. Nouveau integrations/drive_sync.py (Python, réutilise le token OAuth AIOS existant pour le Drive perso, aucun nouveau secret). server.js : synchro déclenchée en arrière-plan après la réponse HTTP (jamais bloquante pour l'équipe), fonctions driveSyncUpload/driveMaybeRefile/monthLabel, appelée depuis /upload, /confirm/:id, /confirm-batch et l'édition dashboard (POST /api/invoices/:id). db.js : colonnes drive_file_id/drive_month/drive_synced_at (migration non destructive). public/index.html : lien "voir sur Drive" dans le tiroir de détail. Testé de bout en bout en local : upload réel → OCR → fichier apparu dans Facturas/Agosto 2026 avec le bon nom, correction de date → fichier déplacé vers Facturas/Julio 2026 confirmé côté API Drive ; données de test nettoyées sur Drive (reste une ligne de test dans data/compta.db, à supprimer manuellement, le hook de garde-fou bloque les DELETE FROM en Bash). Toggle COMPTA_DRIVE_SYNC=0 si besoin de désactiver. Fichiers : integrations/drive_sync.py (nouveau), server.js, db.js, public/index.html.
  • - 2026-08-13 - Splash de reconnexion (fin du flash du login). Ajout d'un écran #boot-splash (marque « Compta » / « MOA », spinner, « Connexion en cours… ») dans public/index.html (dashboard propriétaire). But : supprimer le clignotement du formulaire de login au démarrage, le temps que la session soit revalidée par le token stocké (localStorage compta_owner + revalidation via /api/summary). Le splash s'affiche par défaut au chargement ; une fonction hideSplash() le masque exactement quand l'auth initiale est résolue : succès (fin de boot(), après le rendu de l'app) ET échec/session expirée (fin de login(), après le rendu du formulaire, y compris le chemin 401 -> api() -> login()). Filet de sécurité setTimeout(hideSplash, 4000) près du boot pour ne jamais bloquer l'UI. Palette reprise du thème sombre de l'app (fond #0e1512, texte #eaf1ec, accent vert #2fbd77). Pas de changement sur public/capture.html (page de capture publique par token, sans login, donc sans flash). Front statique : pas de restart. Pas de cache-bust ?v= dans cette app (CSS+JS inline). node --check OK sur le script extrait de index.html. Fichier : public/index.html.
  • - 2026-08-06 - Mise en ligne + mode scan en rafale. (1) Déploiement : app passée en service systemd compta.service (Restart=always, 127.0.0.1:8326) et exposée sur https://compta.panelbay.com (Cloudflare DNS + nginx + Let's Encrypt). .env durci pour la prod : OWNER_PASSWORD régénéré (mot de passe fort), COMPTA_SECRET régénéré (64 hex), COMPTA_BASE_URL=https://compta.panelbay.com (liens de capture équipe corrects), INVOICE_OCR_KEY renseignée avec la clé OpenAI STT partagée du parc (vision OK ; prévoir une clé dédiée pour du volume). client_max_body_size 25m ajouté au vhost nginx (sinon 413 sur PDF/photos > 1 Mo). Ajout au portail new-tab (section Tech & Outils, icône receipt). (2) Mode rafale (public/capture.html réécrit) : écran d'accueil à 2 choix (« Scanner en rafale (plusieurs) » vs « Une seule facture (vérif détaillée) »). En rafale, l'utilisateur enchaîne les photos (bouton Photo une par une, ou Galerie/PDF multi-sélection) ; chaque fichier est uploadé + OCR en arrière-plan (pool de 3 en parallèle, pump()), une file d'attente affiche le statut par ligne (⏳ lecture / ✓ lue / ⚠ erreur avec Réessayer/Retirer), champs corrigeables en ligne (repliés par défaut), un déposant unique appliqué à toute la salve, et une barre fixe « Enregistrer N factures ». Le mode « une facture » détaillé d'origine est conservé. (3) Backend (server.js) : nouveau POST /api/snap/:token/confirm-batch ({uploader, items:[...]}) qui applique déposant + corrections à toute la salve en une transaction SQLite, réutilise parseAmount, scope par company_id. Testé de bout en bout en local (upload réel → OCR correct : FERRETERIA SAN MIGUEL, date, montant, n° → confirm-batch → base à jour ; ligne de test supprimée). Restart service + healthcheck OK, page de capture vérifiée au navigateur (accueil + écran rafale). Fichiers : server.js, public/capture.html, .env, /etc/systemd/system/compta.service, vhost nginx, apps/portail/index.html.
  • - 2026-08-05 - Corrections d'audit (P1/P2 + accessibilité/UI ciblés). Backend server.js : jeton propriétaire signé à expiration (fin du ?t= en URL, header Authorization obligatoire), rate-limit login par IP + backoff + comparaison timingSafeEqual, validation serveur des montants (fini/≥0/plafond), idempotence upload par SHA-256 + dédup (company_id, hash), pagination /api/invoices (limit/offset + total). Nouveau env.js (chargeur .env) + .env créé (valeurs prototype conservées). db.js : colonne file_hash + index + migration non destructive. Front capture.html : esc() sur tous les champs OCR réinjectés (XSS), compression/redimensionnement image côté client (canvas 1600px, JPEG 0.8), skeleton pendant l'extraction, hauteur d'image réservée (CLS). Front index.html : export CSV et image du drawer via fetch+blob (header Authorization), pagination « Charger plus », lignes du registre focusables/activables au clavier + Échap ferme le drawer, tabular-nums + zebra striping. NON traités volontairement : i18n/traduction ES (#9, #10) et logs structurés/taux d'échec (#14), hors périmètre de ce lot. Reporté : aucun autre. Testé : node --check OK sur les 4 fichiers Node, boot local OK, endpoints vérifiés (auth, rate-limit 429, dédup, validation montants, pagination).
  • - 2026-08-02 - Manuel initialisé (bootstrap auto).