Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
Pécule
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Faites pousser votre épargne. Tableau de bord d'épargne personnel, calibré pour la fiscalité française. Sans base de données : vos données restent dans un seul fichier, sur votre propre Google Drive. • Présentation • Fonctionnalités • Architecture • Modèle de données et invariants • Sécurité et confidentialité • Démarrage rapide • Déploiement • Structure du projet • Installer l'app sur mobile • Contribuer • Licence • CréditsREADME 전문 보기 ↗
Faites pousser votre épargne.
Tableau de bord d'épargne personnel, calibré pour la fiscalité française. Sans base de données : vos données restent dans un seul fichier, sur votre propre Google Drive.
Ouvrir l'app · Confidentialité · Nouveautés · Maintenance · Feuille de route · Wiki · Contact
Pécule (anciennement Suivi Épargne) est une application web installable (PWA) pour suivre ses comptes d'épargne, piloter sa paie mois après mois et anticiper sa fiscalité : livrets réglementés, PEA, assurance vie, PEE, PER, crypto.
suivi_epargne.json, sur le Google Drive de l'utilisateur. L'app ne voit que les fichiers qu'elle a créés (portée OAuth drive.file).worker/) apporte ce qu'un navigateur seul ne peut pas faire : une session Google persistante, des notifications push quotidiennes, la veille fiscale hebdomadaire (pages officielles lues par Cloudflare Workers AI) et une sauvegarde de secours chiffrée par l'app, qu'il ne peut pas lire. Sans lui, l'app fonctionne entièrement dans le navigateur.src/lib/askSnapshot.ts : comptes, épargne sur 24 mois et totaux déjà calculés, soldes mensuels, 60 jours à venir, virements, abonnements, paie nette, intérêts, impôt, restitution ; ni e-mail, ni identifiant Drive, ni clé, ni texte libre ; taille plafonnée). Lecture seule ; conversation en mémoire seulement, effacée à la fermeture, au verrouillage et à la déconnexion.importStatement, avec « Annuler »). Lecture fictive en mode démo.public/share-sw.js, Web Share Target déclaré dans le manifeste), gardé sur l'appareil seulement, puis, après le verrou et le chargement des données, Pécule demande « Que voulez-vous importer ? » : fiche de paie (lue depuis l'appareil, sans copie ni lien Drive), relevé bancaire ou avis d'imposition. Rien n'est envoyé à Gemini avant ce choix ; le fichier est effacé dès sa lecture, à la déconnexion, et au bout d'une heure s'il n'a jamais été ouvert.src/lib/yearlyInterest.ts).flowchart LR
Pages[(GitHub Pages<br/>fichiers statiques)]
Edge[Cloudflare : DNS, proxy<br/>Worker pecule-edge<br/>en-têtes de sécurité]
Pages --> Edge
subgraph Navigateur["Navigateur : PWA React (pecule-app.com)"]
UI[Écrans React]
Hook[usePortfolioData<br/>document, synchronisation<br/>et quarantaine]
Lib[src/lib<br/>calculs purs et testés]
Local[(Copie locale chiffrée<br/>AES-GCM, IndexedDB)]
UI --> Hook --> Lib
Hook <--> Local
end
Edge -->|HTML, JS, CSS| UI
Drive[(Google Drive<br/>suivi_epargne.json)]
Gemini[Gemini API<br/>fiches de paie, avis d'imposition,<br/>relevés, Demander à Pécule,<br/>veille fiscale en secours]
SP[service-public.gouv.fr]
subgraph Worker["Cloudflare Worker (facultatif) : suivi-epargne-api"]
Auth[Session<br/>refresh token chiffré → jetons 1 h]
Cron[Cron quotidien 07:00 UTC<br/>rappels → Web Push]
Watch[Cron du lundi 05:00 UTC<br/>veille fiscale]
Backup["Sauvegarde de secours<br/>copies chiffrées par l'app"]
FS[GET /fiscal-sources<br/>cache 3 jours]
KV[(KV STORE)]
AI[Workers AI<br/>Llama 3.3 70B]
Auth --- KV
Cron --- KV
Watch --- KV
Backup --- KV
FS --- KV
Watch --> AI
end
Hook <-->|lecture et écriture directes| Drive
UI -->|clé de l'appareil| Gemini
UI <-->|session| Auth
UI -->|relevé de la veille| Watch
UI -->|copie chiffrée| Backup
UI -->|textes officiels| FS
FS -->|liste fixe d'URL| SP
Watch -->|liste fixe d'URL| SP
Cron -->|lecture seule| Drive
Cron -.->|chiffré RFC 8291| Push[Services push<br/>FCM, Apple, Mozilla, Windows]
Push -.-> UI
fiscal-watch:latest) est comparé aux paramètres par l'app, jamais appliqué d'office.pecule-edge (edge/) n'ajoute que des en-têtes de sécurité (HSTS, anti-iframe, politique de permissions). L'ancienne adresse github.io redirige vers pecule-app.com.src/lib/, src/types.ts, src/constants.ts) : une notification dit exactement ce que dit la bannière correspondante.src/hooks/portfolio/useDriveSync.ts, derrière la façade usePortfolioData) : contrôle de révision Drive (aucune écriture à l'aveugle), écritures en série, quarantaine locale des modifications non synchronisées, coordination entre onglets, démarrage hors ligne sur la dernière copie chiffrée (saisies envoyées au retour du réseau, ou choix si Drive a changé), délais maximaux sur les appels réseau, rechargement automatique quand une nouvelle version est déployée (recherchée aussi au retour au premier plan).Détail du serveur, de ses routes et de son stockage : worker/README.md.
Un seul document JSON (GlobalAppData, src/types.ts) : comptes et mouvements, charges, historiques mensuels, fiches de paie, mouvements récurrents, abonnements, dons, paramètres fiscaux (fiscalConfig), avantages salariaux, configuration.
| Règle | Où |
|---|---|
Tout ce qui entre (Drive, import, restauration, autre onglet, Worker) passe par migrate(). Les champs inconnus sont conservés. | src/lib/schema.ts |
APP_SCHEMA_VERSION n'augmente que si un ancien client deviendrait dangereux. Il refuse alors d'écrire. | src/lib/schema.ts |
Tout changement de solde passe par applyMovement, snapshotBalances, restoreBalances. Total = part propre + part des parents, au centime. | src/lib/accountOps.ts |
kind d'un mouvement : aucun (versement réel), valuation, parental, adjustment. Seul isSavingsFlow décide de ce qui compte comme épargne. | src/lib/finance/savings.ts |
| Le capital des parents n'est jamais proposé comme source d'un retrait. Ses intérêts reviennent à l'utilisateur. | src/lib/finance/interest.ts, restitution.ts |
La clé Gemini ne quitte jamais l'appareil (withoutDeviceOnlyFields, ignorée par canonicalize). | src/lib/schema.ts |
Les noms techniques suivi-epargne (dépôt, URL, fichier Drive, Worker) ne changent pas. | — |
Liste complète et raisons : MAINTENANCE.md §1.
drive.file uniquement (les fichiers créés par l'app), plus openid et email côté serveur pour l'identité. Aucune portée sensible, aucun envoi d'e-mail.index.html : aucun script intégré, scripts Google limités aux fichiers exacts chargés (gsi/client, js/api.js et ses modules), connexions limitées à Google et à l'origine exacte du Worker (injectée au build par vite.config.ts). Test : edge/test/csp.test.ts.worker/src/deviceProof.ts) ; un jeton de session copié seul ne sert à rien (une copie du profil entier du navigateur emporte la clé). Les sessions d'avant la liaison sont refusées (une reconnexion). Paramètres liste les appareils connectés (« Déconnecter »).pecule-edge devant GitHub Pages : HSTS, X-Frame-Options: DENY et frame-ancestors 'none', nosniff, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, et cache adapté (fichiers hachés : un an, immutable ; pages, sw.js, push-sw.js, share-sw.js : no-cache) (détail : MAINTENANCE.md § 8).Signaler une faille : voir SECURITY.md. Contact : [email protected].
Prérequis : Node.js 22+ (version de référence dans .nvmrc).
npm ci # dépendances de l'app
npm ci --prefix worker # dépendances du serveur (nécessaires au typecheck)
npm ci --prefix edge # dépendances du Worker des en-têtes (idem)
npm run dev # http://localhost:5173
npm run lint # ESLint, avec les règles d'accessibilité jsx-a11y (bloquant en CI)
npm run typecheck # types : app + serveur + edge
npm test # tests Vitest : app + serveur (worker/test) + edge (edge/test), en trois groupes (calculs, serveur, écrans : voir vitest.config.ts)
npm run test:coverage # idem, avec les seuils de couverture de vitest.config.ts (comme la CI)
# (chiffres ci-dessous)
npm run test:watch # tests en continu
npm run e2e # tests de bout en bout Playwright (ordinateur et mobile), serveur de dev
# E2E_TARGET=build : les mêmes sur le build de production (comme la CI)
npm run build # build de production dans dist/
npm run preview # sert le build localement
npm run theme # régénère src/theme/m3.css depuis scripts/m3-theme.mjs (palette Material)
Pour npm run e2e, installe une fois le navigateur : npx playwright install chromium (en local, la config utilise Chrome s'il est installé). Le serveur de développement est lancé automatiquement. En CI (ou avec E2E_TARGET=build), Playwright construit le build de production dans dist-e2e/ et le sert avec vite preview : CSP de production, service worker et minification sont donc testés (e2e/target.ts). La CI repasse ensuite e2e/production.spec.ts sur dist, le dossier exact que deploy.yml publie (E2E_DIST=dist, servi tel quel). La plupart des parcours tournent en mode démo ; ceux de la synchronisation (conflit, sauvegarde locale, hors ligne, session expirée, échec de lecture) tournent sans démo, contre un faux Drive en mémoire (e2e/fakeDrive.ts), sans aucun appel à Google ni au serveur. Aucun test ne joint le vrai serveur : les tests importent test depuis e2e/fixtures.ts, qui coupe ces appels (règle ESLint).
Couverture des tests unitaires (mesurée le 28 octobre 2026, seuils plancher dans vitest.config.ts, vérifiés par la CI). Deux chiffres, à ne pas confondre :
| Périmètre | Instructions | Branches | Fonctions | Lignes |
|---|---|---|---|---|
| Toute l'app (écrans, hooks, services, App.tsx, serveur, edge) | 81,7 % | 75,3 % | 73,2 % | 85,1 % |
Cœur métier (src/lib, worker/src, edge/src) | 97,0 % | 90,5 % | 98,1 % | 98,6 % |
| Filets anti-perte (synchronisation Drive, copies locales, sauvegarde de secours, session, verrou) | 91,1 % | 83,9 % | 89,4 % | 95,7 % |
La plupart des écrans ont des tests unitaires (React Testing Library) ; ils sont aussi couverts par les tests de bout en bout, qui ne comptent pas dans ces chiffres.
Mode démo, sans compte Google : http://localhost:5173/?demo=1. L'app se charge avec des données fictives (src/dev/demoData.ts), et rien n'est lu ni écrit sur Drive. Ce mode n'existe qu'en développement et dans le build des tests de bout en bout (dist-e2e/, jamais publié) : dans le build publié, il est retiré du code.
Avec le serveur local : dans worker/, npm run setup-secrets -- --dev (génère .dev.vars), puis npm run dev (port 8787). Ensuite, à la racine, crée .env.development.local :
VITE_BACKEND_URL=http://localhost:8787
Configuration Google Cloud (client OAuth « Application Web », identifiant dans src/services/googleDriveService.ts) :
https://pecule-app.com et http://localhost:5173 ;https://<worker>.workers.dev/auth/callback ;https://pecule-app.com/* : sans cela, le sélecteur de fichiers Drive affiche « The API developer key is invalid » (voir MAINTENANCE.md § 8) ;Avant chaque mise à jour visible, ajoute une entrée en tête de src/changelog.ts : version AAAA.MM.JJ (suffixe -2, -3… le même jour), titre et 1 à 6 phrases simples. Elle déclenche « Quoi de neuf » et la GitHub Release.
| Workflow | Déclencheur | Rôle |
|---|---|---|
ci.yml · Vérifications | PR, chaque lundi à 6 h UTC, et à chaque push sur main via deploy.yml (une seule fois par push) | lint, types, tests avec seuils de couverture (toute l'app, cœur métier, filets anti-perte), tests de bout en bout Playwright sur le build de production, build publié (tailles dans le résumé) testé tel quel par e2e/production.spec.ts puis gardé en artefact site-dist, audit des dépendances (informatif) |
deploy.yml · Déploiement | push sur main, ou lancement manuel (Run workflow, Use workflow from : main, ou un tag pour revenir à une version précédente, sans créer de tag ; voir MAINTENANCE.md § 5) | ci.yml une fois, puis ensemble : GitHub Pages (build testé site-dist repris sans reconstruire), wrangler deploy du serveur (worker/) et du Worker d'en-têtes pecule-edge (edge/ : HSTS, anti-iframe, cache… devant GitHub Pages), à chaque push : app et serveur toujours accordés. Puis tag v<version> et Release avec les puces du changelog (une fois par version) |
monitor.yml · Surveillance | chaque jour à 6 h 30 UTC, ou lancement manuel | site (script principal compris : un écran blanc est détecté), pages publiques, serveur (/status), certificat TLS, HSTS ; ouvre/ferme une issue panne |
.env.production. Le commit déployé est injecté au build (__BUILD_SHA__).CLOUDFLARE_API_TOKEN et CLOUDFLARE_ACCOUNT_ID existent (Settings → Secrets and variables → Actions). Sans eux, le job est sauté, et npm run deploy dans worker/ reste possible. Les secrets du Worker (GOOGLE_CLIENT_SECRET, ENCRYPTION_KEY, VAPID_*, ALLOWED_EMAILS) se posent avec npm run setup-secrets : voir worker/README.md.worker/, edge/, Actions). Toutes les montées majeures npm se font à la main..
├── .github/
│ ├── workflows/ ci.yml · deploy.yml · monitor.yml
│ ├── ISSUE_TEMPLATE/ bug · idée · liens (sécurité, contact, code de conduite)
│ ├── pull_request_template.md
│ └── dependabot.yml app, worker/, edge/, Actions
├── public/ icônes, police, presentation.html, confidentialite.html, push-sw.js, share-sw.js
├── scripts/
│ └── m3-theme.mjs génère la palette Material 3 (npm run theme → src/theme/m3.css)
├── src/
│ ├── components/ écrans (Dashboard, Agenda, Journal, Yield, Simulator, Settings…)
│ │ ├── ui/ composants Material : Card, StatTile, MoneyText, DataTable, Tabs…
│ │ ├── charts/ cadre commun des graphiques (phrase de synthèse, « Voir les données »)
│ │ ├── motivation/ bons mois, jalons, point de paie, réglages
│ │ ├── pilot/ Pilotage : budget, remplissage des livrets, horloge fiscale…
│ │ ├── simulator/ « Et si… » : éditeur de scénario, graphique
│ │ ├── update/ Actualiser les soldes, une ligne par compte
│ │ ├── year/ « Votre année » et clôture de l'année
│ │ └── dashboard/ Accueil : carte d'épargne, cartes et « Plus de détails », barre
│ │ disponible / avec impôt / bloqué
│ ├── app/ morceaux d'App : session, verrou, navigation, bannières et fenêtres
│ │ de synchronisation, écran courant (mémoïsé), actions sur les données
│ ├── hooks/ usePortfolioData (façade), useFiscalWatch, useTheme…
│ │ └── portfolio/ document et setters, synchronisation Drive, copies locales chiffrées
│ ├── lib/ calculs purs et testés
│ │ ├── commands/ opérations qui changent les soldes, avec leur annulation exacte
│ │ ├── sync/ état de la synchronisation (transitions nommées, indicateur)
│ │ ├── finance/ income · capitalTax · interest · savings · placement ·
│ │ │ subscriptions · payPlan · donations · fiscalParams · restitution
│ │ ├── finance.ts réexporte finance/
│ │ ├── accountOps.ts seule voie de modification des soldes
│ │ ├── schema.ts migrate(), version du format, canonicalize
│ │ ├── fiscalWatch.ts veille fiscale (propositions bornées et sourcées)
│ │ ├── cloudBackupCrypto.ts chiffrement de la sauvegarde de secours, code de secours
│ │ ├── motivation.ts · simulator.ts · alerts.ts · yearReview.ts · driveWriteCheck.ts
│ │ ├── home.ts (Accueil) · yearClosing.ts (clôture de l'année) · statementImport.ts (relevés)
│ │ ├── accountTypes.ts · freshness.ts · performance.ts · inflation.ts · retirement.ts (PER)
│ │ ├── csv.ts · storageKeys.ts · theme.ts · parentalMode.ts · idleCash.ts · projectionEngine.ts
│ │ └── lep.ts · notificationPrefs.ts · agenda.ts · planning.ts · taxReturn.ts · projection.ts …
│ ├── services/ Google (Drive, Picker), Gemini, serveur, sauvegarde de secours,
│ │ push, verrou, coffre local
│ ├── theme/m3.css rôles de couleur Material (généré)
│ ├── workers/ Web Worker du simulateur (fourchette calculée hors de l'écran)
│ ├── dev/ données du mode démo
│ ├── changelog.ts historique des mises à jour (« Quoi de neuf », Releases)
│ └── constants.ts · types.ts · navigation.ts · App.tsx · index.tsx · index.css (@theme Tailwind 4)
├── e2e/ tests Playwright : parcours en mode démo (alertes, motivation,
│ restitution, simulateur, actualiser, votre année, comptes, relevés,
│ clôture de l'année, virements, affichage, contrastes), synchronisation
│ contre un faux Drive (fakeDrive.ts), build publié (production.spec.ts)
├── worker/ serveur Cloudflare : voir worker/README.md
│ ├── src/ index (routes, crons) · google · sessions · deviceProof · security ·
│ │ crypto · subscriptions · webpush · reminders · fiscalSources ·
│ │ fiscalWatchJob · backupRetention · siteCheck · userRecords
│ ├── test/ tests Vitest (OAuth, sessions, push RFC 8291, rappels, sources,
│ │ veille, sauvegardes, /status)
│ ├── scripts/ setup-secrets.mjs
│ └── wrangler.toml KV, limiteurs, Workers AI, crons
├── edge/ Worker pecule-edge : en-têtes de sécurité devant GitHub Pages
│ ├── src/index.ts · test/ · wrangler.toml (routes pecule-app.com)
├── MAINTENANCE.md · ROADMAP.md · CONTRIBUTING.md · SECURITY.md · CODE_OF_CONDUCT.md
├── index.html (CSP) · vite.config.ts (PWA, CSP) · vitest.config.ts · playwright.config.ts
├── eslint.config.js · tsconfig.json
└── .nvmrc · .env.production
Identité visuelle : Material 3 Expressive, couleur source vert sapin #14532d (variante « fidélité »), or #fbbf24 en tertiaire. La palette est générée par npm run theme (scripts/m3-theme.mjs → src/theme/m3.css) : seuls les rôles Material sont disponibles en classes (bg-surface-container, text-on-surface-variant, text-primary, text-inverse-error…), en clair et en sombre ; aucune échelle Tailwind numérotée (indigo, slate…) n'est rebranchée. Formes, graisses, élévations et mouvements dans src/index.css. Police Google Sans Flex, y compris pour les montants (chiffres de largeur fixe, jamais de police à chasse fixe : font-mono pointe vers elle ; un code à recopier lettre par lettre prend font-code). Logo vectoriel : src/components/Logo.tsx.
Couleurs de sens : trois rôles Material personnalisés, déclarés une seule fois dans scripts/m3-theme.mjs (rapprochés du vert sapin par Blend.harmonize) et générés en clair et en sombre : gain (hausse), loss (baisse) et parents (part des parents, même or que le tertiaire). Chacun a sa couleur, son conteneur et leurs textes : text-gain, bg-loss-container text-on-loss-container, text-parents… src/lib/themeTokens.test.ts en garde le contraste AA. Un écran n'écrit jamais de vert, rouge ou ambre brut pour dire « gain », « perte » ou « parents ». Plus généralement, aucune classe de palette brute (bg-slate-800, text-indigo-700…), sans exception : le lint (no-restricted-syntax dans eslint.config.js, chaînes et gabarits de src) les refuse pendant l'écriture, et src/components/paletteGuard.test.ts vérifie tout src, src/theme/m3.css et src/index.css. En dernier filet, src/index.css retire du thème les échelles numérotées de Tailwind (--color-slate-*: initial…) : une classe brute qui passerait ne produirait aucun style. Le test vérifie que le lint et ce retrait couvrent toutes les palettes et tous les utilitaires de couleur de la version installée de Tailwind (à relancer après une mise à jour de Tailwind).
Boutons : Button (variantes filled, tonal, outlined, text, danger ; taille sm de 32 px pour une action secondaire dans une ligne), ui/IconButton pour une icône seule, ui/Chip pour une puce. Un bouton texte a 12 px de marge intérieure (un -ml-3 aligne son texte au bord). Restent des <button> faits main là où le kit n'a pas d'équivalent : barre et rail de navigation, bouton flottant, en-têtes repliables, lignes de liste cliquables, clavier du code, interrupteur, barres du graphique d'épargne, actions de la notification (surface inversée) et liens en ligne dans une phrase.
Thème : « Automatique » (par défaut) suit le réglage clair ou sombre de l'appareil, y compris en direct ; « Clair » et « Sombre » sont des choix explicites, enregistrés seulement quand l'utilisateur les fait (Paramètres, carte « Affichage », ou le bouton soleil/lune de l'en-tête). Règles pures dans src/lib/theme.ts, application à la page (classe dark, couleur de la barre d'état meta theme-color toujours accordée au thème affiché) dans src/hooks/useTheme.ts. Avant le démarrage de l'app, public/theme-boot.js (petit fichier à part : la CSP interdit un script intégré) pose déjà la classe dark selon la même règle, pour éviter un éclair clair ; src/lib/themeBoot.test.ts vérifie qu'il décide comme src/lib/theme.ts.
Cibles tactiles : 48 px au moins au doigt. Sur écran tactile, src/index.css étend la zone sensible de tout bouton, interrupteur, onglet ou résumé à 48 px sans changer son aspect ; la classe touch-target fait de même pour un lien ou une étiquette cliquable. Un bouton-icône a toujours un libellé (ui/IconButton, aria-label et infobulle). e2e/display.spec.ts vérifie la zone réellement touchable (Accueil et Paramètres, téléphone).
Pécule est un projet personnel. Les signalements et suggestions sont les bienvenus via les issues, ou à [email protected]. Avant toute pull request, lis CONTRIBUTING.md, les invariants de MAINTENANCE.md et le code de conduite. Une PR doit passer npm run lint, npm run typecheck, npm run test:coverage, npm run e2e et npm run build. Toute modification visible ajoute une entrée au changelog, et tout chiffre fiscal cite sa source officielle. Le guide d'utilisation est dans le wiki.
GNU Affero General Public License v3.0 ou ultérieure (AGPL-3.0-or-later). Vous pouvez utiliser, modifier et redistribuer le code. En contrepartie, toute version modifiée, y compris proposée en ligne comme service, doit être publiée sous la même licence, avec son code source.
Le nom « Pécule » et le logo ne sont pas couverts par la licence : une version modifiée doit porter un autre nom.
Pécule n'est pas un conseiller financier : les calculs sont des estimations à vérifier auprès des sources officielles.
__Host-id_token vérifié ;ALLOWED_EMAILS, revérifiée à chaque requête ;invocation_logs = false : aucune adresse IP enregistrée) ;src/lib/cloudBackupCrypto.ts, src/services/cloudBackup.ts.