# Spec — Simpl-Resultat Web

> Date: 2026-03-30
> Projet: simpl-resultat
> Statut: Draft
> Dependance: Logto IdP (spec-compte-maximus dans la-compagnie-maximus)

## Contexte

Simpl-Resultat est actuellement une app desktop Tauri v2 (Windows/Linux) avec stockage SQLite local et protection par PIN Argon2. L'objectif est de rendre l'application disponible via le web, permettant aux utilisateurs connectes avec un Compte Maximus d'acceder a leurs donnees financieres depuis un navigateur.

L'app desktop continue de fonctionner en offline sans compte. Le compte et le web sont optionnels.

**Decision securite** : Les donnees financieres sont stockees en clair cote serveur (Option D — chiffrement au repos + isolation forte). L'E2EE zero-knowledge a ete evalue et rejete pour la v1 car incompatible avec les fonctionnalites web (recherche serveur, rapports, import CSV). L'hebergement local au Quebec (VPS OVH Beauharnois, Loi 25) est le differenciateur securite, pas le zero-knowledge. L'E2EE reste une option premium future ("mode Coffre-fort").

## Objectif

Permettre aux utilisateurs de gerer leur budget et transactions depuis un navigateur web a `resultat.lacompagniemaximus.com`, avec possibilite de synchronisation avec l'app desktop.

## Scope

### IN
- API REST backend pour toutes les operations CRUD (transactions, categories, budgets, ajustements, imports, fournisseurs, keywords)
- Schema PostgreSQL (migration des 13 tables SQLite, schema `sr_`)
- Frontend web porte depuis le code React/Vite/Tailwind existant (bonne reutilisabilite)
- Import CSV cote serveur (upload → parsing → auto-categorisation → preview → confirmation)
- Auth via le service auth dedie (Compte Maximus)
- Multi-profil par utilisateur (comme l'app desktop)
- Hebergement sur Coolify (resultat.lacompagniemaximus.com)
- i18n FR/EN
- Dark mode
- Securite Option D (chiffrement au repos + isolation)

### OUT
- E2EE zero-knowledge (decision documentee, option premium future)
- Connexion bancaire directe (Plaid/Flinks — future phase)
- App mobile simpl-resultat
- Partage de budget entre utilisateurs (ex: conjoint — future phase)
- Notifications/alertes serveur sur depassements budgetaires (future phase)
- Export PDF de rapports (future phase)

## Design

### Schema PostgreSQL (schema `sr_`)

Migration des 13 tables SQLite existantes vers PostgreSQL. Changements principaux :
- Ajout de `user_id` et `profile_id` pour isoler les donnees par utilisateur et par profil
- Ajout d'une table `sr_profiles` pour gerer les profils multiples par utilisateur (remplace le systeme de fichiers de profils dans Tauri)
- UUID comme cles primaires au lieu de INTEGER AUTOINCREMENT
- TIMESTAMPTZ au lieu de TEXT/DATETIME pour les dates
- NUMERIC(12,2) au lieu de REAL pour les montants (precision financiere)
- Conservation de la structure hierarchique des categories (parent_id)
- Conservation du systeme d'auto-categorisation par mots-cles
- Seed des 54 categories par defaut et 60+ keywords par profil

```sql
CREATE SCHEMA IF NOT EXISTS sr_;

-- Profils utilisateur (remplace le systeme fichiers Tauri)
CREATE TABLE sr_.profiles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,  -- FK vers le service auth (Compte Maximus)
    name TEXT NOT NULL,
    pin_hash TEXT,           -- Argon2 hash, optionnel sur le web
    currency TEXT NOT NULL DEFAULT 'CAD',
    date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(user_id, name)
);

-- Sources d'import (configurations de parsing)
CREATE TABLE sr_.import_sources (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    date_format TEXT NOT NULL DEFAULT '%d/%m/%Y',
    delimiter TEXT NOT NULL DEFAULT ';',
    encoding TEXT NOT NULL DEFAULT 'utf-8',
    column_mapping JSONB NOT NULL,
    skip_lines INTEGER NOT NULL DEFAULT 0,
    has_header BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(profile_id, name)
);

-- Fichiers importes (deduplication)
CREATE TABLE sr_.imported_files (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    source_id UUID NOT NULL REFERENCES sr_.import_sources(id) ON DELETE CASCADE,
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    filename TEXT NOT NULL,
    file_hash TEXT NOT NULL,
    import_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    row_count INTEGER NOT NULL DEFAULT 0,
    status TEXT NOT NULL DEFAULT 'completed',
    notes TEXT,
    UNIQUE(source_id, filename)
);

-- Categories hierarchiques (parent_id, pre-seeded)
CREATE TABLE sr_.categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    parent_id UUID REFERENCES sr_.categories(id) ON DELETE SET NULL,
    color TEXT,
    icon TEXT,
    type TEXT NOT NULL DEFAULT 'expense',  -- 'expense' | 'income'
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    is_inputable BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Fournisseurs (normalises pour matching)
CREATE TABLE sr_.suppliers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    normalized_name TEXT NOT NULL,
    category_id UUID REFERENCES sr_.categories(id) ON DELETE SET NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(profile_id, normalized_name)
);

-- Mots-cles pour auto-categorisation (pre-seeded)
CREATE TABLE sr_.keywords (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    keyword TEXT NOT NULL,
    category_id UUID NOT NULL REFERENCES sr_.categories(id) ON DELETE CASCADE,
    supplier_id UUID REFERENCES sr_.suppliers(id) ON DELETE SET NULL,
    priority INTEGER NOT NULL DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    UNIQUE(profile_id, keyword, category_id)
);

-- Transactions (avec support split via parent_transaction_id)
CREATE TABLE sr_.transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    date DATE NOT NULL,
    description TEXT NOT NULL,
    amount NUMERIC(12,2) NOT NULL,
    category_id UUID REFERENCES sr_.categories(id) ON DELETE SET NULL,
    supplier_id UUID REFERENCES sr_.suppliers(id) ON DELETE SET NULL,
    source_id UUID REFERENCES sr_.import_sources(id) ON DELETE SET NULL,
    file_id UUID REFERENCES sr_.imported_files(id) ON DELETE SET NULL,
    original_description TEXT,
    notes TEXT,
    is_manually_categorized BOOLEAN NOT NULL DEFAULT FALSE,
    is_split BOOLEAN NOT NULL DEFAULT FALSE,
    parent_transaction_id UUID REFERENCES sr_.transactions(id) ON DELETE CASCADE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Ajustements (ponctuels ou recurrents)
CREATE TABLE sr_.adjustments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    date DATE NOT NULL,
    is_recurring BOOLEAN NOT NULL DEFAULT FALSE,
    recurrence_rule TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Entrees d'ajustement (montant par categorie)
CREATE TABLE sr_.adjustment_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    adjustment_id UUID NOT NULL REFERENCES sr_.adjustments(id) ON DELETE CASCADE,
    category_id UUID NOT NULL REFERENCES sr_.categories(id) ON DELETE CASCADE,
    amount NUMERIC(12,2) NOT NULL,
    description TEXT
);

-- Entrees budgetaires (grille 12 mois par categorie)
CREATE TABLE sr_.budget_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    category_id UUID NOT NULL REFERENCES sr_.categories(id) ON DELETE CASCADE,
    year INTEGER NOT NULL,
    month INTEGER NOT NULL,  -- 1-12
    amount NUMERIC(12,2) NOT NULL,
    notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(profile_id, category_id, year, month)
);

-- Templates de budget (reutilisables)
CREATE TABLE sr_.budget_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(profile_id, name)
);

-- Entrees de template de budget
CREATE TABLE sr_.budget_template_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    template_id UUID NOT NULL REFERENCES sr_.budget_templates(id) ON DELETE CASCADE,
    category_id UUID NOT NULL REFERENCES sr_.categories(id) ON DELETE CASCADE,
    amount NUMERIC(12,2) NOT NULL,
    UNIQUE(template_id, category_id)
);

-- Templates de configuration d'import
CREATE TABLE sr_.import_config_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    delimiter TEXT NOT NULL DEFAULT ';',
    encoding TEXT NOT NULL DEFAULT 'utf-8',
    date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY',
    skip_lines INTEGER NOT NULL DEFAULT 0,
    has_header BOOLEAN NOT NULL DEFAULT TRUE,
    column_mapping JSONB NOT NULL,
    amount_mode TEXT NOT NULL DEFAULT 'single',      -- 'single' | 'dual'
    sign_convention TEXT NOT NULL DEFAULT 'negative_expense',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(profile_id, name)
);

-- Preferences utilisateur (cle-valeur par profil)
CREATE TABLE sr_.user_preferences (
    profile_id UUID NOT NULL REFERENCES sr_.profiles(id) ON DELETE CASCADE,
    key TEXT NOT NULL,
    value TEXT NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (profile_id, key)
);

-- Journal d'audit (securite)
CREATE TABLE sr_.audit_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    profile_id UUID,
    action TEXT NOT NULL,       -- 'login', 'export', 'delete', 'import', 'bulk_delete'
    entity_type TEXT,          -- 'transaction', 'profile', 'category', etc.
    entity_id UUID,
    metadata JSONB,            -- details supplementaires (ex: nombre de lignes importees)
    ip_address INET,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index
CREATE INDEX idx_sr_profiles_user ON sr_.profiles(user_id);
CREATE INDEX idx_sr_transactions_profile_date ON sr_.transactions(profile_id, date);
CREATE INDEX idx_sr_transactions_category ON sr_.transactions(category_id);
CREATE INDEX idx_sr_transactions_supplier ON sr_.transactions(supplier_id);
CREATE INDEX idx_sr_transactions_source ON sr_.transactions(source_id);
CREATE INDEX idx_sr_transactions_file ON sr_.transactions(file_id);
CREATE INDEX idx_sr_transactions_parent ON sr_.transactions(parent_transaction_id);
CREATE INDEX idx_sr_categories_profile_parent ON sr_.categories(profile_id, parent_id);
CREATE INDEX idx_sr_categories_type ON sr_.categories(profile_id, type);
CREATE INDEX idx_sr_suppliers_profile_category ON sr_.suppliers(profile_id, category_id);
CREATE INDEX idx_sr_suppliers_normalized ON sr_.suppliers(profile_id, normalized_name);
CREATE INDEX idx_sr_keywords_profile_category ON sr_.keywords(profile_id, category_id);
CREATE INDEX idx_sr_keywords_keyword ON sr_.keywords(profile_id, keyword);
CREATE INDEX idx_sr_budget_entries_period ON sr_.budget_entries(profile_id, year, month);
CREATE INDEX idx_sr_adjustment_entries_adjustment ON sr_.adjustment_entries(adjustment_id);
CREATE INDEX idx_sr_imported_files_source ON sr_.imported_files(source_id);
CREATE INDEX idx_sr_audit_log_user ON sr_.audit_log(user_id, created_at);
CREATE INDEX idx_sr_audit_log_profile ON sr_.audit_log(profile_id, created_at);

-- Preferences par defaut (inserees a la creation d'un profil)
-- INSERT INTO sr_.user_preferences (profile_id, key, value) VALUES
--   ($profile_id, 'language', 'fr'),
--   ($profile_id, 'theme', 'light'),
--   ($profile_id, 'currency', 'CAD'),
--   ($profile_id, 'date_format', 'DD/MM/YYYY');
```

### API REST

Base URL: `https://resultat.lacompagniemaximus.com/api`
Auth: Bearer token (JWT depuis Logto)
Tous les endpoints scopes par `user_id` (extrait du JWT) + `profile_id` (header ou parametre)

#### Profils
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/profiles` | Lister les profils de l'utilisateur |
| POST | `/profiles` | Creer un profil (seed categories + keywords) |
| GET | `/profiles/:id` | Details d'un profil |
| PUT | `/profiles/:id` | Modifier un profil (nom, devise, format date) |
| DELETE | `/profiles/:id` | Supprimer un profil et toutes ses donnees |
| POST | `/profiles/:id/verify-pin` | Verifier le PIN (couche securite optionnelle) |

#### Transactions
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/transactions` | Lister avec filtres (date range, category_id, supplier_id, search, min/max amount, page, limit) |
| POST | `/transactions` | Creer une transaction manuelle |
| GET | `/transactions/:id` | Detail d'une transaction |
| PUT | `/transactions/:id` | Modifier (description, categorie, notes, montant) |
| DELETE | `/transactions/:id` | Supprimer |
| POST | `/transactions/:id/split` | Splitter en plusieurs entrees par categorie |
| DELETE | `/transactions/bulk` | Suppression en lot (body: { ids: [] }) |

#### Categories
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/categories` | Arbre hierarchique complet |
| POST | `/categories` | Creer une categorie custom |
| PUT | `/categories/:id` | Modifier (nom, couleur, icone, parent, sort_order) |
| DELETE | `/categories/:id` | Supprimer (SET NULL sur les transactions liees) |
| PUT | `/categories/reorder` | Reorganiser l'ordre des categories |

#### Fournisseurs & Mots-cles
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/suppliers` | Lister les fournisseurs |
| POST | `/suppliers` | Creer un fournisseur |
| PUT | `/suppliers/:id` | Modifier |
| DELETE | `/suppliers/:id` | Supprimer |
| GET | `/keywords` | Lister les mots-cles |
| POST | `/keywords` | Creer un mot-cle |
| PUT | `/keywords/:id` | Modifier |
| DELETE | `/keywords/:id` | Supprimer |

#### Budgets
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/budgets/:year` | Grille budgetaire 12 mois pour une annee |
| PUT | `/budgets` | Mettre a jour des entrees budgetaires (batch) |
| GET | `/budgets/templates` | Lister les templates de budget |
| POST | `/budgets/templates` | Creer un template |
| PUT | `/budgets/templates/:id` | Modifier un template |
| DELETE | `/budgets/templates/:id` | Supprimer un template |
| POST | `/budgets/apply-template` | Appliquer un template a une annee |

#### Ajustements
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/adjustments` | Lister les ajustements |
| POST | `/adjustments` | Creer un ajustement (avec entrees) |
| GET | `/adjustments/:id` | Detail d'un ajustement |
| PUT | `/adjustments/:id` | Modifier |
| DELETE | `/adjustments/:id` | Supprimer |

#### Import CSV
| Methode | Endpoint | Description |
|---------|----------|-------------|
| POST | `/import/upload` | Upload un fichier CSV (stockage temporaire) |
| POST | `/import/parse` | Parser le CSV avec config (delimiter, encoding, date_format, column mapping) |
| GET | `/import/preview/:upload_id` | Preview des transactions parsees avec auto-categorisation |
| POST | `/import/confirm` | Confirmer l'import (inserer les transactions) |
| GET | `/import/sources` | Configurations de sources d'import |
| POST | `/import/sources` | Sauvegarder une config de source |
| PUT | `/import/sources/:id` | Modifier une config |
| DELETE | `/import/sources/:id` | Supprimer une config |
| GET | `/import/history` | Historique des imports |
| GET | `/import/config-templates` | Templates de configuration d'import |
| POST | `/import/config-templates` | Creer un template de config |

#### Rapports (rendus possibles par Option D)
| Methode | Endpoint | Description |
|---------|----------|-------------|
| GET | `/reports/monthly-summary/:year/:month` | Totaux par categorie, budget vs reel |
| GET | `/reports/trends` | Tendances mensuelles sur une periode (query: start, end, category_ids) |
| GET | `/reports/category-breakdown` | Repartition des depenses par categorie (query: start, end) |

### Frontend Web

C'est le gros avantage : l'app Tauri existante est deja **React 19 + Vite + Tailwind CSS v4 + TypeScript**. Les composants UI, hooks et logique metier sont hautement reutilisables. Changements principaux :

- Remplacer les commandes Tauri (Rust → SQLite) par des appels API (fetch → REST)
- Remplacer `@tauri-apps/plugin-sql` par un client API
- Remplacer les operations systeme de fichiers (import CSV depuis fichier local) par un upload fichier
- Remplacer la selection de profil Tauri → auth web + selecteur de profil
- Conserver tous les composants React : table de transactions, arbre de categories, grille budget, graphiques (Recharts), wizard import
- Conserver `react-router-dom`, `i18next`, `lucide-react`, `recharts`, `papaparse` (pour preview client), `@dnd-kit` (drag-and-drop)
- Retirer les dependances Tauri : `@tauri-apps/api`, `@tauri-apps/plugin-*`

Heberge sur Coolify a `resultat.lacompagniemaximus.com`.

### Securite (Option D — detaillee)

Puisque l'app traite des donnees financieres sensibles, voici le modele de securite complet :

1. **Chiffrement au repos** : Repertoire de donnees PostgreSQL sur volume chiffre (LUKS/dm-crypt sur le VPS)
2. **TLS en transit** : HTTPS obligatoire, headers HSTS
3. **Isolation du datastore** :
   - Schema `sr_` separe des autres apps dans PostgreSQL
   - Credentials PostgreSQL dedies au service simpl-resultat (pas de superuser partage)
   - Le service n'a pas acces a Forgejo, MinIO (sauf pour import CSV temporaire), ni aux autres schemas
4. **Isolation par utilisateur** : Toutes les requetes filtrees par `user_id` + `profile_id`. Pas de requetes cross-user possibles.
5. **Backups chiffres** : `pg_dump` du schema `sr_` chiffre avec une cle stockee hors du VPS (ex: sur la machine locale ou dans un secret manager)
6. **Politique d'acces** : Jamais de consultation des donnees utilisateurs sans consentement explicite. Pas de dashboard admin montrant les donnees financieres des utilisateurs.
7. **Audit log** : Table `sr_.audit_log` (user_id, action, entity, timestamp) pour tracer les acces sensibles (exports, suppressions, imports)
8. **Rate limiting** : Sur tous les endpoints, particulierement import CSV (upload) et auth
9. **Input validation** : Sanitization de toutes les entrees, particulierement les descriptions de transactions et les notes
10. **Session** : JWT configure dans Logto (access token ~1h, refresh token ~14j), SDK gere le refresh automatiquement

**E2EE future ("mode Coffre-fort")** : Option premium ou le profil entier est chiffre cote client (AES-256-GCM, cle derivee du mot de passe utilisateur). Le serveur stocke un blob opaque. Incompatible avec les fonctionnalites serveur (rapports, search, import CSV serveur). L'utilisateur choisit : fonctionnalites completes OU privacy maximale.

### Gestion d'acces

- Acces a l'app web reserve aux utilisateurs avec un Compte Maximus
- Plan gratuit : acces basique (illimite pour la v1, pas de paywall)
- Plan premium (futur) : import CSV illimite, rapports avances, multi-profil, backup automatique
- Verification du JWT claims `apps.simpl-resultat` pour confirmer l'acces
- Profile-level access : un utilisateur peut avoir plusieurs profils (personnel, couple, etc.)
- PIN optionnel par profil sur le web (couche de securite supplementaire, comme sur desktop)

### Sync Desktop <-> Web

Deux approches, la plus simple pour la v1 :

**Option A — Export/Import (v1, simple)** [retenue]
- L'app desktop a deja un export/import SREF (AES-256-GCM chiffre)
- Ajouter un bouton "Synchroniser avec le cloud" dans l'app desktop
- Upload le fichier SREF vers le serveur, le serveur dechiffre et importe
- Pas de sync temps reel, mais suffisant pour un backup/migration

**Option B — Sync continue (v2, complexe)** [future]
- Change tracking sur chaque table (updated_at + sync tokens)
- Sync bidirectionnelle comme simpl-liste
- Conflict resolution par entite
- Plus complexe a cause du nombre de tables (13) et des relations

Recommandation : Option A pour la v1 (reutilise l'infrastructure export/import existante), Option B comme amelioration future.

## Plan de travail

### Issue 1 — Schema PostgreSQL et migrations [type:task]
Dependances: Logto deploye et operationnel
- [ ] Creer le schema `sr_` dans PostgreSQL (15 tables, index, contraintes)
- [ ] Table sr_profiles (multi-profil par utilisateur)
- [ ] Table sr_audit_log
- [ ] Script de seed : 54 categories + 60+ keywords par profil
- [ ] Script de migration
- [ ] Tests du schema (contraintes, cascades, unicite)

### Issue 2 — API REST backend [type:feature]
Dependances: Issue 1
- [ ] Setup projet (Next.js ou Express sur Coolify)
- [ ] Middleware auth (verification JWT, extraction user_id, injection profile_id)
- [ ] Endpoints profils (CRUD + PIN)
- [ ] Endpoints transactions (CRUD + filtres + splits + bulk delete)
- [ ] Endpoints categories (CRUD + arbre hierarchique + reorder)
- [ ] Endpoints fournisseurs + keywords
- [ ] Endpoints budgets (grille mensuelle + templates + apply)
- [ ] Endpoints ajustements (CRUD avec entrees)
- [ ] Endpoints rapports (monthly summary, trends, category breakdown)
- [ ] Rate limiting + input validation
- [ ] Audit logging middleware

### Issue 3 — Import CSV serveur [type:feature]
Dependances: Issue 2
- [ ] Upload CSV vers stockage temporaire (MinIO ou /tmp)
- [ ] Parsing multi-encoding (UTF-8, Windows-1252, ISO-8859-15) comme l'app desktop
- [ ] Auto-detection delimiteur
- [ ] Column mapping configurable
- [ ] Auto-categorisation par keywords (reproduire la logique Rust existante)
- [ ] Preview avant confirmation
- [ ] Deduplication par filename + file_hash (comme l'app desktop)
- [ ] Gestion des import sources et config templates

### Issue 4 — Frontend web [type:feature]
Dependances: Issue 2, Issue 3
- [ ] Setup projet (React + Vite + Tailwind CSS v4, port de la config existante)
- [ ] Client API (wrapper fetch avec auth JWT, gestion erreurs, profile_id)
- [ ] Auth flow (login via Logto, SDK @logto/react ou OIDC standard)
- [ ] Selecteur de profil
- [ ] Page transactions (table, filtres, tri, recherche, pagination)
- [ ] Page detail transaction (edition, re-categorisation, split)
- [ ] Saisie manuelle de transactions
- [ ] Page categories (arbre hierarchique, CRUD, drag-and-drop)
- [ ] Page budget (grille 12 mois, budget vs reel, templates)
- [ ] Page ajustements (CRUD, recurrence)
- [ ] Wizard import CSV (port des 13 etapes existantes vers upload serveur)
- [ ] Page rapports (graphiques Recharts : tendances, repartition par categorie)
- [ ] Page fournisseurs et keywords
- [ ] Dark mode
- [ ] i18n FR/EN (reutiliser les fichiers de traduction existants)
- [ ] Responsive
- [ ] Deployer sur Coolify (resultat.lacompagniemaximus.com)

### Issue 5 — Sync desktop <-> web (v1: export/import) [type:feature]
Dependances: Issue 2
- [ ] Ajouter bouton "Synchroniser avec le cloud" dans l'app desktop Tauri
- [ ] Auth flow desktop : OAuth2 via Logto (OIDC standard, tauri-plugin-oauth ou WebView)
- [ ] Export SREF → upload vers le serveur
- [ ] Le serveur dechiffre le SREF et importe dans le schema sr_
- [ ] Download depuis le serveur → import SREF dans l'app desktop
- [ ] Indicateur de derniere synchronisation

### Issue 6 — Securite et audit [type:task]
Dependances: Issue 2
- [ ] Configurer le chiffrement au repos du volume PostgreSQL (LUKS)
- [ ] Credentials PostgreSQL dedies (pas de superuser partage)
- [ ] Configurer les backups chiffres du schema sr_
- [ ] Implementer sr_audit_log (middleware automatique)
- [ ] Headers de securite (HSTS, CSP, X-Frame-Options, X-Content-Type-Options)
- [ ] Tests de securite (injection SQL, XSS, IDOR cross-user)

### Ordre d'execution

```
Logto (prerequis externe)
    └── Issue 1 (Schema PostgreSQL)
            ├── Issue 2 (API REST)
            │       ├── Issue 3 (Import CSV)
            │       ├── Issue 5 (Sync desktop)
            │       └── Issue 6 (Securite)
            │
            └── Issue 4 (Frontend web) — depends on Issue 2 + Issue 3
```

## Criteres d'acceptation

- [ ] Un utilisateur connecte peut acceder a ses finances depuis resultat.lacompagniemaximus.com
- [ ] Les operations CRUD fonctionnent pour les transactions, categories, budgets, ajustements
- [ ] L'import CSV fonctionne cote serveur (upload, parsing, auto-categorisation, preview, confirmation)
- [ ] Les rapports (tendances, repartition) s'affichent correctement avec des graphiques
- [ ] Le multi-profil fonctionne (creer, switcher, supprimer des profils)
- [ ] L'app desktop continue de fonctionner sans compte (offline-first preserve)
- [ ] La sync export/import entre desktop et web fonctionne
- [ ] Le site fonctionne en FR et EN, en light et dark mode
- [ ] Le site est responsive
- [ ] Les donnees financieres sont isolees des autres services (schema separe, credentials dedies)
- [ ] Un audit log trace les acces sensibles

## Estimation

8-12 sessions de travail (hors service auth). C'est le plus gros chantier des apps web en raison de la complexite du modele de donnees (13 tables desktop → 15 tables web) et de la logique metier (import CSV, auto-categorisation, budgets, rapports).

## Reutilisabilite du code existant

| Composant | Reutilisable ? | Notes |
|-----------|---------------|-------|
| Composants React (53 fichiers) | **Oui** | Deja React 19 + Tailwind CSS v4, memes primitives que le web |
| Recharts (graphiques) | **Oui** | Librairie web native, memes graphiques |
| Types TypeScript | **Oui** | Interfaces, enums, types de donnees |
| Hooks custom (12 hooks useReducer) | **Partiellement** | Adapter les appels services → fetch API |
| Services metier (14 services) | **Partiellement** | Remplacer tauri-plugin-sql par client API REST |
| i18n (traductions FR/EN) | **Oui** | Fichiers JSON reutilisables directement |
| @dnd-kit (drag-and-drop) | **Oui** | Librairie web native |
| papaparse (parsing CSV) | **Oui** | Utile pour preview cote client |
| lucide-react (icones) | **Oui** | Librairie web native |
| react-router-dom (routing) | **Oui** | Meme routing |
| Commandes Rust (17 commandes Tauri) | **Non** | Remplacees par l'API REST |
| Crypto Rust (Argon2, AES-256-GCM) | **Non** | PIN optionnel reimplemente en JS si necessaire |
| Import CSV Rust (parsing) | **Non** | Reimplemente cote serveur (Node.js) |
| tauri-plugin-sql | **Non** | Remplace par client API |
| tauri-plugin-updater | **Non** | Non applicable au web |
