Wiki/docs/15-baserow-mpd.md
Corentin JOGUET 668576cdc4 chore: initial commit — formation-hub conception phase
Conception complete (Phase 0) pour formation-hub Acadenice :

- 19 docs Merise Agile + UML + GitOps + plans (tests/deploy/ops/api)
  cf docs/00-readme.md pour l'index complet
- Stack Docker compose (Docmost + Baserow + Postgres + Redis + MinIO local FS)
  compose.yml + compose.staging.yml + compose.prod.yml
- CI/CD GitHub Actions skeleton (ci, deploy-staging, deploy-prod)
- Bridge service skeleton (Hono + TS + Biome + Vitest + zod + pino)
- Templates GitHub : PR + 3 issue types + CODEOWNERS + dependabot.yml
- Scripts ops : healthcheck, backup quotidien, smoke-test post-deploy
- LICENSE AGPL-3.0 + SECURITY.md + CONTRIBUTING.md + CHANGELOG.md
- Diagramme drawIO archi infra (XML importable dans diagrams.net)

Decisions structurelles enregistrees :
- Scope CFA + Agence avec entite PERSONNE pivot multi-roles (ADR-001)
- Stack composite Docmost AGPL + Baserow MIT + bridge custom (ADR-001)
- Path B : UX quasi-unified via Tiptap node-views custom (ADR-002)
- Monorepo trunk-based development (ADR-003)
- Postgres separe Docmost/Baserow (ADR-004)
- Bridge stack Node 22 + Hono (ADR-005)
- Repo neuf prefere a fork Docmost
- Prod-like des le jour 1 (pas MVP)
2026-05-07 12:16:19 +02:00

21 KiB

MPD — Modele Physique de Donnees (Baserow)

Implementation concrete dans Baserow : 9 tables avec types exacts, formules, vues, permissions. Ce doc est actionnable : Corentin l'ouvre cote a cote avec Baserow et cree les tables une par une. Source : 07-merise-mld.md (MLD relationnel) + 05-data-dictionary.md.

1. Setup initial

1.1 Hierarchie Baserow

Workspace : Acadenice formation-hub
└── Database : formation-hub
    ├── Tables CFA : formation, bloc, module, attribution
    ├── Tables Agence : client, projet, tache, intervention
    └── Table pivot : personne

1.2 Order de creation (dependances FK)

Creer les tables dans cet ordre — chaque table dependant des precedentes via FK :

  1. personne (aucune dep)
  2. client (aucune dep)
  3. formation (aucune dep)
  4. bloc (FK → formation)
  5. projet (FK → client, optionnel FK → formation)
  6. module (FK → bloc)
  7. tache (FK → projet)
  8. attribution (FK → module + personne)
  9. intervention (FK → tache + personne)

1.3 Conventions Baserow

Concept Implementation Baserow
ID auto-increment (PK) Baserow id natif (genere auto)
Nom du field snake_case, prefixes par mnemonique d'entite (ex formation_nom, personne_email)
Field "Primary" Baserow oblige a avoir un Primary field — choisir le nom le plus explicite (ex formation_nom)
Foreign Key Field type Link to table
ENUM Field type Single select avec options exactes
MULTI ENUM Field type Multiple select
Champ calcule (formula) Field type Formula
Champ derive d'une relation Field type Lookup ou Count
Audit timestamps Fields Created on + Last modified time natifs
Audit acteur Fields Created by + Last modified by natifs

1.4 API token

Apres creation des tables :

  • Settings → API tokens → Create token
  • Permissions : read/create/update/delete sur la database formation-hub
  • Stocker dans .env cote bridge : BASEROW_API_TOKEN=...

2. Table personne

Primary field : personne_nom (texte affiche par defaut dans les links)

# Field Type Baserow Parametres Description
1 personne_nom Text Nom de famille (Primary)
2 personne_prenom Text Prenom
3 personne_email Email Email pro (unique recommande, validation cote bridge)
4 personne_telephone Phone number Telephone (optionnel)
5 personne_capacite_annuelle Number Decimal places: 2 Heures totales/an
6 personne_split_formation_pct Number Decimal places: 1, default 50 % capacite alloue formation
7 personne_split_agence_pct Number Decimal places: 1, default 50 % capacite alloue agence
8 personne_roles Multiple select Options: formateur, developpeur, admin, direction, support Roles cumules
9 personne_statut Single select Options: actif (default), inactif Statut
10 personne_attributions Link to table Lien vers attribution (champ inverse auto-cree apres creation de attribution) Toutes les attributions de cette personne
11 personne_interventions Link to table Lien vers intervention (apres creation) Toutes les interventions
12 personne_heures_attribuees_formation Formula sum(lookup('personne_attributions', 'attribution_heures_attribuees_active')) Rollup des attributions actives
13 personne_heures_attribuees_agence Formula sum(lookup('personne_interventions', 'intervention_heures_active')) Rollup des interventions actives
14 personne_heures_restantes_formation Formula (field('personne_capacite_annuelle') * field('personne_split_formation_pct') / 100) - field('personne_heures_attribuees_formation') Capacite formation restante
15 personne_heures_restantes_agence Formula (field('personne_capacite_annuelle') * field('personne_split_agence_pct') / 100) - field('personne_heures_attribuees_agence') Capacite agence restante
16 personne_heures_restantes_total Formula field('personne_capacite_annuelle') - field('personne_heures_attribuees_formation') - field('personne_heures_attribuees_agence') Capacite totale restante

Vues recommandees :

  • Tous (grid, default) — tableau complet
  • Actifs (grid, filtre personne_statut = actif)
  • Formateurs (grid, filtre personne_roles contient formateur)
  • Developpeurs (grid, filtre personne_roles contient developpeur)
  • Capacite restante (grid, sort personne_heures_restantes_total ascending)

3. Table formation

Primary field : formation_nom

# Field Type Baserow Parametres Description
1 formation_nom Text Nom (Primary, unique conseille)
2 formation_description Long text rich text autorise Description longue
3 formation_filiere Single select dev, graphisme, marketing, iot, cybersec Filiere
4 formation_heures_totales Number Decimal places: 2 Heures totales prevues
5 formation_statut Single select draft (default), actif, termine, archive Cycle de vie
6 formation_date_debut Date format YYYY-MM-DD Date debut
7 formation_date_fin Date Date fin
8 formation_blocs Link to table Lien vers bloc (apres creation bloc) Blocs de la formation
9 formation_projets_pedagogiques Link to table Lien vers projet (optionnel) Projets agence lies en pedagogique
10 formation_heures_attribuees Formula sum(lookup('formation_blocs', 'bloc_heures_prevues')) Rollup heures des blocs
11 formation_heures_restantes Formula field('formation_heures_totales') - field('formation_heures_attribuees') Reste a attribuer
12 formation_created_at Created on Audit
13 formation_updated_at Last modified time Audit

Vues :

  • Tous (grid)
  • Actives (grid, filtre formation_statut = actif)
  • Par filiere (grid, group by formation_filiere)
  • Calendrier (calendar view, sur formation_date_debut)
  • Capacite restante (grid, sort formation_heures_restantes ascending)

4. Table bloc

Primary field : bloc_nom

# Field Type Baserow Parametres Description
1 bloc_nom Text Nom du bloc (Primary)
2 bloc_description Long text Description
3 bloc_formation Link to table Lien vers formation (single) Formation parente
4 bloc_heures_prevues Number Decimal places: 2 Heures du bloc
5 bloc_ordre Number Decimal places: 0 Ordre dans la formation
6 bloc_modules Link to table Lien vers module (apres creation) Modules du bloc
7 bloc_heures_attribuees Formula sum(lookup('bloc_modules', 'module_heures_prevues_active')) Rollup heures modules actifs
8 bloc_heures_restantes Formula field('bloc_heures_prevues') - field('bloc_heures_attribuees') Reste a decomposer

Note : la regle metier "un bloc a un nom unique par formation" se valide cote bridge ou via une vue filtree de duplication.

Vues :

  • Tous (grid)
  • Par formation (grid, group by bloc_formation)

5. Table module

Primary field : module_nom

# Field Type Baserow Parametres Description
1 module_nom Text Nom du module (Primary)
2 module_description Long text Description
3 module_bloc Link to table Lien vers bloc (single) Bloc parent
4 module_heures_prevues Number Decimal places: 2 Heures du module
5 module_statut Single select a_attribuer (default), attribue, en_cours, realise, annule Cycle de vie
6 module_attributions Link to table Lien vers attribution (apres creation) Attributions du module
7 module_heures_prevues_active Formula if(field('module_statut') = 'annule', 0, field('module_heures_prevues')) Pour rollup bloc (exclut annule)
8 module_heures_attribuees Formula sum(lookup('module_attributions', 'attribution_heures_attribuees_active')) Rollup
9 module_heures_realisees Formula sum(lookup('module_attributions', 'attribution_heures_realisees')) Rollup

Vues :

  • Tous (grid)
  • A attribuer (kanban, group by module_statut) — vue principale pour l'admin
  • Par bloc (grid, group by module_bloc)
  • Realises (grid, filtre module_statut = realise)

6. Table attribution

Primary field : attribution_titre (formula : nom_module + " → " + nom_personne)

# Field Type Baserow Parametres Description
1 attribution_titre Formula concat(lookup('attribution_module', 'module_nom'), ' → ', lookup('attribution_personne', 'personne_prenom'), ' ', lookup('attribution_personne', 'personne_nom')) Titre auto (Primary)
2 attribution_module Link to table Lien vers module (single) Module attribue
3 attribution_personne Link to table Lien vers personne (single) Formateur (role formateur requis)
4 attribution_heures_attribuees Number Decimal places: 2 Heures planifiees
5 attribution_heures_realisees Number Decimal places: 2, default 0 Heures effectuees
6 attribution_date_debut Date Debut periode
7 attribution_date_fin Date Fin periode
8 attribution_statut Single select planifie (default), en_cours, realise, annule Statut
9 attribution_heures_attribuees_active Formula if(field('attribution_statut') = 'annule', 0, field('attribution_heures_attribuees')) Pour rollup module/personne

Validation cote bridge :

  • attribution_personne.personne_roles doit contenir formateur
  • sum(attribution_heures_attribuees) for module <= module_heures_prevues (RG-01)

Vues :

  • Tous (grid)
  • Mes attributions (grid, filtre attribution_personne = current user) — vue formateur
  • En cours (grid, filtre attribution_statut = en_cours)
  • Calendrier (calendar view sur attribution_date_debut ou attribution_date_fin)
  • Form public (form view) — formateur saisit ses heures realisees rapide

7. Table client

Primary field : client_nom

# Field Type Baserow Parametres Description
1 client_nom Text Nom (Primary)
2 client_contact_principal Text Nom + role du contact
3 client_contact_email Email Email contact
4 client_contact_telephone Phone number Telephone
5 client_secteur Text Secteur d'activite
6 client_notes Long text Notes libres
7 client_statut Single select prospect (default), actif, inactif, archive Statut
8 client_projets Link to table Lien vers projet Projets du client
9 client_created_at Created on Audit

Vues :

  • Tous (grid)
  • Actifs (grid, filtre client_statut = actif)
  • Pipeline (kanban, group by client_statut) — vue commerciale

8. Table projet

Primary field : projet_nom

# Field Type Baserow Parametres Description
1 projet_nom Text Nom (Primary)
2 projet_description Long text Description
3 projet_client Link to table Lien vers client (single) Client
4 projet_type Single select site_web, app_mobile, api, infra, audit, support, autre Type
5 projet_charge_heures Number Decimal places: 2 Charge estimee
6 projet_date_debut Date Date debut
7 projet_date_fin_prevue Date Date fin prevue
8 projet_date_livraison Date Date livraison effective
9 projet_statut Single select devis (default), en_cours, livre, cloture, abandonne Statut
10 projet_formation_pedagogique Link to table Lien vers formation (single, optionnel) Lien pedagogique
11 projet_url URL Site livraison
12 projet_repository URL Repo Git
13 projet_taches Link to table Lien vers tache Taches du projet
14 projet_heures_attribuees Formula sum(lookup('projet_taches', 'tache_charge_heures')) Rollup taches
15 projet_heures_realisees Formula sum(lookup('projet_taches', 'tache_heures_realisees')) Rollup
16 projet_heures_restantes Formula field('projet_charge_heures') - field('projet_heures_realisees') Reste a faire

Vues :

  • Tous (grid)
  • Pipeline (kanban, group by projet_statut) — vue principale
  • En cours (grid, filtre projet_statut = en_cours)
  • Timeline (timeline view sur date_debut → date_fin_prevue)
  • Par client (grid, group by projet_client)

9. Table tache

Primary field : tache_titre

# Field Type Baserow Parametres Description
1 tache_titre Text Titre (Primary)
2 tache_description Long text Description
3 tache_projet Link to table Lien vers projet (single) Projet parent
4 tache_charge_heures Number Decimal places: 2 Charge estimee
5 tache_priorite Single select faible, normale, haute, critique Priorite
6 tache_statut Single select todo (default), in_progress, review, done, abandoned Statut
7 tache_date_debut Date Debut prevu
8 tache_date_fin_prevue Date Fin prevue
9 tache_assignee Link to table Lien vers personne (single, optionnel) Dev assignee informellement
10 tache_interventions Link to table Lien vers intervention Interventions sur cette tache
11 tache_heures_realisees Formula sum(lookup('tache_interventions', 'intervention_heures_active')) Rollup

Vues :

  • Tous (grid)
  • Kanban (kanban, group by tache_statut) — vue principale
  • Par priorite (grid, group by tache_priorite, sort par priorite)
  • Mes taches (grid, filtre tache_assignee = current user)
  • Done recentes (grid, filtre tache_statut = done, sort par date desc)

10. Table intervention

Primary field : intervention_titre (formula auto)

# Field Type Baserow Parametres Description
1 intervention_titre Formula concat(lookup('intervention_tache', 'tache_titre'), ' - ', lookup('intervention_personne', 'personne_prenom'), ' (', totext(field('intervention_date')), ')') Titre auto (Primary)
2 intervention_tache Link to table Lien vers tache (single) Tache concernee
3 intervention_personne Link to table Lien vers personne (single) Developpeur (role developpeur requis)
4 intervention_heures Number Decimal places: 2 Heures effectuees
5 intervention_date Date default today Date intervention
6 intervention_notes Long text Notes / commit ref / lien PR
7 intervention_statut Single select planifie, realise (default), annule Statut
8 intervention_heures_active Formula if(field('intervention_statut') = 'annule', 0, field('intervention_heures')) Pour rollup tache/personne

Validation cote bridge :

  • intervention_personne.personne_roles doit contenir developpeur
  • intervention_heures > 0

Vues :

  • Tous (grid, sort intervention_date desc)
  • Mes interventions (grid, filtre intervention_personne = current user) — vue dev
  • Form rapide (form public) — saisie heures rapide mobile
  • Par projet (grid, group by intervention_tache.tache_projet)
  • Cette semaine (grid, filtre intervention_date >= start_of_week)

11. Permissions et sharing

11.1 Roles Baserow

Role Membres Capacites
Admin workspace Corentin, Yan, Ludo Plein controle
Editor Sophie + autres admins Read/write toutes tables
Builder Formateurs / Devs Read/write leur ligne via vues filtrees + form rapide
Viewer Stakeholders ponctuels Read seul

Limitation : Baserow native permissions sont au niveau database/table, pas row-level. Pour limiter formateur/dev a leurs propres rows :

  • Soit vues filtrees partagees publiquement (form pour saisie + grid filtree pour lecture)
  • Soit bridge service qui filtre cote API selon current_user_id

11.2 Forms publics pour saisie rapide

Plus simple que de gerer les permissions row-level :

  • Form public sur attribution — formateur saisit ses heures via lien sans compte Baserow
  • Form public sur intervention — dev saisit son intervention idem

Le user qui saisit n'a pas besoin de voir le reste des donnees, juste son formulaire.


12. Webhooks (Phase 2 — bridge integration)

Configurer dans Baserow → Database Settings → Webhooks :

Evenement URL cible Usage bridge
row.created sur attribution https://bridge.acadenice.fr/webhooks/attribution-created Notif formateur, recalcul cache mention
row.updated sur attribution https://bridge.acadenice.fr/webhooks/attribution-updated Recalcul cache, notif si statut change
row.created sur intervention https://bridge.acadenice.fr/webhooks/intervention-created Notif admin si depassement capacite
row.updated sur module (si module_statut change) https://bridge.acadenice.fr/webhooks/module-status-changed Trigger cloturer formation auto

Authentification webhook : header X-Bridge-Token avec un secret partage (.env).


13. Seed data initial

Apres creation des 9 tables, seed avec :

  • personne : equipe Acadenice (Yan, Corentin, Ludo, Sophie + formateurs intervenants)
  • client : 1-2 clients existants (Centralis Europe + autre)
  • formation : les 5 filieres en cours pour 2026-2027
  • bloc : decoupage RNCP par filiere
  • module : programme detaille
  • projet : projets clients en cours
  • Pas d'attribution / intervention seed — saisies par l'usage

Script de seed : baserow/seed/seed.py (a coder Phase 1).


14. Validation post-creation

Checklist apres creation des 9 tables :

  • Les 9 tables existent dans la database formation-hub
  • Toutes les FK sont liees correctement (verifier en cliquant sur un lien dans une row)
  • Les rollups fonctionnent (creer une row test, verifier le calcul de formation_heures_attribuees)
  • Les formulas s'evaluent sans erreur (regarder les rows test)
  • Les Single Select / Multiple Select ont les bonnes options
  • Au moins une vue par table est creee (grid Tous minimum)
  • Les vues kanban (module, projet, tache, client) sont fonctionnelles
  • Le form public pour saisie heures (attribution, intervention) marche
  • L'API token est genere et fonctionne (test curl)
# Test API token
curl -H "Authorization: Token $BASEROW_API_TOKEN" \
     "$BASEROW_URL/api/database/rows/table/<TABLE_ID>/?user_field_names=true"

15. Notes d'implementation

15.1 Limitations Baserow connues

  • Pas de FK ON DELETE configurable : c'est SET NULL par defaut quand le lien est rompu. Pour forcer un comportement CASCADE/RESTRICT, le bridge service doit l'implementer (ou un workflow Baserow).
  • Pas de CHECK constraint : validation cote bridge ou cote UI.
  • Pas d'index custom : Baserow indexe automatiquement les Link to table et les Primary fields.
  • Formules limitees : pas de boucles ni de subqueries complexes. Pour calculs lourds, calcul cote bridge + ecriture en batch.

15.2 Alternatives si Baserow limite

Si une formule devient trop complexe ou si on a besoin de validation forte :

  • Option A : Bridge fait le calcul et ecrit en Baserow via API
  • Option B : Vue filtree dediee + formula simple
  • Option C : Migration vers Postgres direct (futur — si on perd Baserow)

15.3 Migration data initiale

Si donnees existent dans Excel/Trello/autre :

  1. Exporter en CSV
  2. Mapper les colonnes vers les fields Baserow
  3. Importer via Baserow UI (Import data)
  4. Verifier les liens FK manuellement (Baserow ne mappe pas auto les liens via CSV)

16. Resume — checklist d'implementation Phase 1

[ ] 1. Setup workspace + database
[ ] 2. Creer table 'personne' (sans liens encore)
[ ] 3. Creer table 'client' (sans liens encore)
[ ] 4. Creer table 'formation' (sans liens encore)
[ ] 5. Creer table 'bloc' + lien vers formation
[ ] 6. Creer table 'projet' + lien vers client (+ optionnel formation)
[ ] 7. Creer table 'module' + lien vers bloc
[ ] 8. Creer table 'tache' + lien vers projet (+ optionnel personne assignee)
[ ] 9. Creer table 'attribution' + liens vers module + personne
[ ] 10. Creer table 'intervention' + liens vers tache + personne
[ ] 11. Ajouter formulas et lookups (apres tous les liens crees)
[ ] 12. Creer vues recommandees par table
[ ] 13. Configurer permissions roles + sharing
[ ] 14. Seed data initial
[ ] 15. Generer API token + verifier
[ ] 16. Documenter exports JSON dans `baserow/schemas/`

Apres ca : la base structurelle est en place. La saisie metier peut commencer immediat — sans attendre Phase 2 / bridge.