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)
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 :
- personne (aucune dep)
- client (aucune dep)
- formation (aucune dep)
- bloc (FK → formation)
- projet (FK → client, optionnel FK → formation)
- module (FK → bloc)
- tache (FK → projet)
- attribution (FK → module + personne)
- 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/deletesur la databaseformation-hub - Stocker dans
.envcote 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 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 completActifs(grid, filtrepersonne_statut = actif)Formateurs(grid, filtrepersonne_roles contient formateur)Developpeurs(grid, filtrepersonne_roles contient developpeur)Capacite restante(grid, sortpersonne_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, filtreformation_statut = actif)Par filiere(grid, group byformation_filiere)Calendrier(calendar view, surformation_date_debut)Capacite restante(grid, sortformation_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 bybloc_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 bymodule_statut) — vue principale pour l'adminPar bloc(grid, group bymodule_bloc)Realises(grid, filtremodule_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_rolesdoit contenirformateursum(attribution_heures_attribuees) for module <= module_heures_prevues(RG-01)
Vues :
Tous(grid)Mes attributions(grid, filtreattribution_personne = current user) — vue formateurEn cours(grid, filtreattribution_statut = en_cours)Calendrier(calendar view surattribution_date_debutouattribution_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 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, filtreclient_statut = actif)Pipeline(kanban, group byclient_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 byprojet_statut) — vue principaleEn cours(grid, filtreprojet_statut = en_cours)Timeline(timeline view sur date_debut → date_fin_prevue)Par client(grid, group byprojet_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 bytache_statut) — vue principalePar priorite(grid, group bytache_priorite, sort par priorite)Mes taches(grid, filtretache_assignee = current user)Done recentes(grid, filtretache_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_rolesdoit contenirdeveloppeurintervention_heures > 0
Vues :
Tous(grid, sortintervention_date desc)Mes interventions(grid, filtreintervention_personne = current user) — vue devForm rapide(form public) — saisie heures rapide mobilePar projet(grid, group byintervention_tache.tache_projet)Cette semaine(grid, filtreintervention_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
Tousminimum) - 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 DELETEconfigurable : c'estSET NULLpar 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 :
- Exporter en CSV
- Mapper les colonnes vers les fields Baserow
- Importer via Baserow UI (
Import data) - 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.