doc: split Architecture.md into per-topic pages

doc/Architecture.md was 436 lines and growing; this commit
extracts each non-trivial subject into its own page under
doc/architecture/ and reduces the root document to a table of
contents + transversal sections (vision, stack, admin rights).

New pages (under doc/architecture/):

- workflow-multi-parties.md : client / fournisseur /
  coordinateur roles, sous-traitance, project states, B2B/B2C,
  domaine musical production flow.
- domaine-musical.md : titres collaboratifs (formats, flux de
  production, contraintes de licence).
- licences.md : LicenceModele, CC/ODbL seed, badge projet,
  cycle de vie.
- domaines-activite.md : arbre des activites, Droit a la
  racine, DomaineActivite model.
- dictionnaires-metier.md : regle d'heritage, DictionnaireMetier
  + TermeMetier, cycle de vie d'un terme. Absorbs the previous
  doc/Dictionnaire.md draft.
- offres-frontmatter.md : ClasseFormulaire / ClasseDevis,
  OffreFournisseur, Demande, parsing YamlDotNet (introduit dans
  4034c399 Front matters). Absorbs the previous
  doc/Formulaires-devis.md and doc/Demande.md fragments.
- postit-oidc.md : documentation du client desktop PostIt,
  custom URI scheme (RFC 8252 §7.1), composants partages,
  plateformes, UX observable, persistance et reprise au boot,
  garanties testees.

Architecture.md (436 -> 66 lines) keeps the vision, the stack
overview, the admin rights section, and a TOC table pointing at
each detail page. The "A documenter ensuite" backlog is kept
at the end.

Cross-links are relative: from Architecture.md the links go
architecture/<page>.md; from inside doc/architecture/ they go
../Architecture.md or <sibling>.md.
This commit is contained in:
Paul Schneider 2026-06-27 13:45:31 +01:00
commit 74de6aa1d1
11 changed files with 548 additions and 338 deletions

View file

@ -0,0 +1,68 @@
# Dictionnaires métier
> **Récapitulatif** : Les dictionnaires sont des modèles publics
> de termes métier, validés par les modérateurs et **copiés**
> dans chaque projet (instantané, pas référence vivante).
> L'héritage suit l'arbre des activités. Détail dans cette page,
> racine de l'architecture : [Architecture.md](../Architecture.md).
## Principe
Les dictionnaires sont des **modèles publics** de termes métier,
maintenus par les modérateurs et alimentés par les fournisseurs.
Au moment de créer un projet, le dictionnaire est **copié**
c'est un instantané, pas une référence vivante.
> Note historique : une première ébauche de cette page existait
> à `doc/Dictionnaire.md`. Elle a été absorbée ici.
## Règle de résolution
> Un projet hérite des dictionnaires de son activité **et de tous
> ses ancêtres** jusqu'à la racine.
Ainsi, les termes juridiques sont toujours disponibles, quel que
soit le domaine du projet.
Le modèle de dictionnaire du contrat est le dictionnaire défini
par l'activité ciblée par le client.
## Dictionnaires juridiques
Les dictionnaires juridiques sont des modèles publics de termes
juridiques, maintenus et alimentés par les modérateurs. Ils
figurent à la racine de l'arbre (sous `Droit`) et profitent donc
à toutes les activités par construction.
> Ce dictionnaire juridique est simplement fondamental, par
> construction, c'est la base de tous les dictionnaires.
## Modèle
```
DictionnaireMetier
- Id, Nom
- DomaineActiviteId (FK)
- Langue ← attribut du dictionnaire entier
TermeMetier
- Id
- DictionnaireMetierId (FK)
- Mot
- Definition
- StatutValidation ← Proposé | Validé | Rejeté
- ProposeParId (FK → ApplicationUser)
- ValidéParId (FK → ApplicationUser, nullable)
```
## Cycle de vie d'un terme
1. Un **fournisseur** propose un terme dans son domaine et sa langue
2. Un **modérateur** valide → le terme intègre le dictionnaire public
3. À la création d'un projet, le dictionnaire est **associé par copie**
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.
- [domaines-activite.md](domaines-activite.md) — l'arbre des
activités qui justifie la règle d'héritage.

View file

@ -0,0 +1,43 @@
# Domaine musical — Titres collaboratifs
> **Récapitulatif** : Yavsc permet de produire collaborativement
> des titres musicaux sous licence libre, en s'appuyant sur la
> mise en relation multi-parties. Détail dans cette page, racine
> de l'architecture : [Architecture.md](../Architecture.md).
## Objectif
Permettre la production collaborative de titres musicaux sous
licence libre, à partir de la mise en relation assurée par Yavsc.
## Formats supportés
- Audio (ex : WAV, FLAC, MP3)
- Partition (ex : MusicXML, LilyPond, PDF)
- MIDI
## Flux de production
1. Un client exprime un besoin musical
2. Des prestataires répondent avec des devis
3. Un **consensus est établi en amont** entre le client et les
contributeurs sur la licence du livrable final
4. La collaboration produit les fichiers
5. Le titre est publié sous la licence choisie
## Contraintes spécifiques
- L'accord de licence sur le livrable musical est une instance
du modèle `LicenceModele` détaillé dans
[licences.md](licences.md).
- L'arborescence d'activités musicales (`Musique → Jazz`,
`Musique → Classique`, etc.) est décrite dans
[domaines-activite.md](domaines-activite.md).
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.
- [workflow-multi-parties.md](workflow-multi-parties.md) — rôle
client/fournisseur/coordinateur dans la production musicale.
- [licences.md](licences.md) — modèle de licence applicable au
livrable.

View file

@ -0,0 +1,59 @@
# Domaines d'activité
> **Récapitulatif** : Yavsc organise ses activités en arbre, avec
> `Droit` à la racine pour rendre les termes juridiques
> disponibles partout. Pas de booléen "transversal" : la
> transversalité découle de la position dans l'arbre. Détail dans
> cette page, racine de l'architecture : [Architecture.md](../Architecture.md).
## Hiérarchie
Les activités sont organisées en arbre. Chaque activité peut avoir
une activité parente d'ordre plus général :
```
Droit ← racine, ancêtre commun
├── Musique
│ ├── Jazz
│ ├── Classique
│ └── ...
├── Graphisme
├── BTP
└── ...
```
Le domaine **Droit** est positionné à la racine — *nul n'est
censé ignorer la loi*. Sa position structurelle rend ses
dictionnaires naturellement disponibles dans tous les projets,
sans attribut spécial.
## Modèle `DomaineActivite`
```
DomaineActivite
- Id, Nom
- ParentId (FK → DomaineActivite, nullable)
```
Pas de booléen `EstTransversal` — la transversalité est une
conséquence de la position dans l'arbre, pas un attribut
explicite.
## Conséquences sur les autres référentiels
Cette arborescence irrigue :
- les **dictionnaires métier** (cf. [dictionnaires-metier.md](dictionnaires-metier.md)) :
un projet hérite des dictionnaires de ses ancêtres.
- les **classes de formulaire et de devis** (cf.
[offres-frontmatter.md](offres-frontmatter.md)) : un fournisseur
rattaché à `Musique → Jazz` a accès aux modèles de son
activité et de ses ancêtres.
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.
- [dictionnaires-metier.md](dictionnaires-metier.md) — règle
d'héritage en arbre.
- [offres-frontmatter.md](offres-frontmatter.md) — modèle
canonique nom/valeur pour formulaires et devis.

View file

@ -0,0 +1,42 @@
# Licences
> **Récapitulatif** : Yavsc référence des modèles de licence
> (Creative Commons + ODbL) maintenus par l'administration. Chaque
> projet signe l'accord avant toute production. Détail dans cette
> page, racine de l'architecture : [Architecture.md](../Architecture.md).
## Modèle `LicenceModele`
Géré par l'administration Yavsc. Chaque modèle porte :
- `EstLibre` (bool) — détermine le badge affiché sur le projet
- Les conditions : attribution, partage à l'identique, usage
commercial, modification
- Une URL vers le texte officiel de la licence
## Seed initial
Licences Creative Commons préchargées : CC0, CC BY, CC BY-SA,
CC BY-ND, CC BY-NC, CC BY-NC-SA, CC BY-NC-ND (toutes en version
4.0) + ODbL 1.0.
## Badge projet
Rendu CSS/HTML — vert si `EstLibre`, orange sinon.
## Cycle de vie
1. L'administration crée ou met à jour un `LicenceModele`.
2. À la création d'un projet, le client choisit un modèle
dans la liste des licences applicables à son activité.
3. Le projet passe par l'état **Devis en cours** avant que
l'accord ne soit signé par toutes les parties (cf.
[workflow-multi-parties.md](workflow-multi-parties.md)).
4. Une fois signé, l'état passe à **En production** ; le badge
du projet reflète `EstLibre`.
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.
- [workflow-multi-parties.md](workflow-multi-parties.md) —
contrainte clé : accord signé avant production.

View file

@ -0,0 +1,106 @@
# Offre fournisseur, formulaire, devis
> **Récapitulatif** : Le fournisseur rédige son offre en Markdown
> avec un en-tête frontmatter YAML qui pointe vers un
> `ClasseFormulaire` et un `ClasseDevis` définis par les
> modérateurs. La demande du client instancie les champs du
> formulaire. L'héritage suit l'arbre des activités. Détail dans
> cette page, racine de l'architecture : [Architecture.md](../Architecture.md).
> Note historique : des fragments existaient à `doc/Formulaires-devis.md`
> et `doc/Demande.md`. Ils sont absorbés ici.
## Principe
Un fournisseur décrit son offre en **Markdown**, avec un en-tête
structuré délimité par `---` (frontmatter YAML standard) qui porte
les métadonnées de formulaire et de devis. Le corps est libre.
```markdown
---
formulaire: Prestation Musicale
devis: Arrangement Orchestral
---
Je propose des arrangements pour orchestre de chambre,
livraison sous 3 semaines, formats MusicXML et PDF...
```
## `ClasseFormulaire` et `ClasseDevis`
Ces deux référentiels sont **définis côté serveur par les
modérateurs**, pour servir de modèles de formulaires et de devis.
Les fournisseurs s'y conforment — ils ne peuvent pas en créer
de nouveaux.
## Modèle canonique nom/valeur
```
ClasseFormulaire
- Id, Nom
- DomaineActiviteId (FK)
└── ChampDemande
- Nom (ex : "tempo", "tonalité", "durée")
- TypeValeur (texte | entier | décimal | booléen | enum)
- Obligatoire (bool)
- ValeurDefaut
ClasseDevis
- Id, Nom
- DomaineActiviteId (FK)
OffreFournisseur
- Id
- FournisseurId (FK → ApplicationUser)
- DomaineActiviteId (FK)
- ContenuMarkdown ← corps libre de l'offre
- ClasseFormulaireId (FK) ← extrait du frontmatter à la sauvegarde
- ClasseDevisId (FK) ← extrait du frontmatter à la sauvegarde
```
## Demande client
À partir de l'offre, la demande instancie les champs du formulaire :
```
Demande
- Id
- OffreFournisseurId (FK)
- ClientId (FK → ApplicationUser)
└── ValeurChampDemande
- ChampDemandeId (FK)
- Valeur (string — sérialisé selon TypeValeur)
```
## Règle d'héritage
Les `ClasseFormulaire` et `ClasseDevis` disponibles pour une
activité incluent ceux de ses **ancêtres** dans l'arbre —
cohérent avec la règle de résolution des dictionnaires
([dictionnaires-metier.md](dictionnaires-metier.md)).
## Parsing du frontmatter avec YamlDotNet
Le parsing du frontmatter vit dans
`src/Yavsc.Server/Services/FrontmatterParser.cs` (introduit dans
le commit `4034c399 Front matters`). Squelette :
```csharp
var parts = markdown.TrimStart()
.Substring(3)
.Split(new[] { "\n---" }, 2,
StringSplitOptions.None);
var yamlBlock = parts[0].Trim();
var corps = parts[1].Trim();
var meta = deserializer.Deserialize<FrontmatterOffreResult>(yamlBlock);
```
Le résultat `FrontmatterOffreResult` est sérialisé en JSON pour
alimenter `OffreFournisseur.ClasseFormulaireId` /
`ClasseDevisId` à la sauvegarde.
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.
- [domaines-activite.md](domaines-activite.md) — arbre des
activités et héritage.

View file

@ -0,0 +1,133 @@
# PostIt — Client desktop et authentification OIDC
> **Récapitulatif** : PostIt est un client desktop Avalonia 12
> multi-cible (Desktop, Android, Browser/WASM) qui consomme l'API
> Yavsc.Org. Authentification **public client + PKCE** via
> **custom URI scheme** (RFC 8252 §7.1) — pas de loopback HTTP.
> Détail dans cette page, racine de l'architecture : [Architecture.md](../Architecture.md).
## Rôle
PostIt est un client desktop (Avalonia 12, multi-cible : Desktop
Linux/Windows, Android, Browser/WASM) qui consomme l'API
Yavsc.Org pour les opérations CRUD du blog et d'autres
ressources de l'utilisateur. C'est un **public client** : pas de
secret, le secret est remplacé par PKCE.
## Flow d'authentification
L'authentification suit **RFC 8252 §7.1** (OAuth 2.0 for Native
Apps — Custom URI Scheme Redirect), pas le pattern loopback
historique. La séquence :
1. L'utilisateur clique **Se connecter** sur `LoginPage`.
2. `YavscApiClient.LoginInteractiveAsync` demande à `OidcClient`
de calculer l'authorize URL et de la passer à
`CustomSchemeBrowser.InvokeAsync`.
3. `CustomSchemeBrowser` ouvre le navigateur système sur l'authorize
URL via `Process.Start(options.StartUrl) { UseShellExecute = true }`,
puis **attend** sur un `TaskCompletionSource<string>` alimenté
par un named pipe `PostIt.OidcCallback`.
4. L'utilisateur s'authentifie sur l'IdP. Le navigateur redirige
vers `postit://callback?code=***&state=…`.
5. L'OS, qui a le scheme `postit://` enregistré, **lance une 2ᵉ
instance** de PostIt avec l'URL en argument.
6. La 2ᵉ instance lit `Environment.GetCommandLineArgs()` *avant*
qu'Avalonia ne boote (`PostIt.Desktop.Program.Main`),
détecte l'URL via `SchemeUrlDetector.FindCallbackUrl`, l'écrit
sur le named pipe puis sort par `Environment.Exit(0)`. Aucune
fenêtre n'est créée.
7. L'instance vivante reçoit l'URL via le pipe, complète la TCS,
`OidcClient` échange le code contre les tokens (access +
refresh + id) et les persiste dans `~/.config/PostIt/tokens.json`.
## Pourquoi le scheme custom plutôt que loopback
Le loopback (`http://127.0.0.1:7890/callback`) oblige l'app à
ouvrir un port TCP, à le publier comme redirect URI dans l'IdP,
et à gérer la course "le navigateur revient-il à temps ?".
Le scheme custom transfère la livraison du callback à l'OS :
le navigateur tape sur une URL que l'OS sait router vers PostIt,
pas vers un serveur HTTP.
## Composants partagés (`PostIt/`)
| Composant | Rôle |
|---------------------------------|-------------------------------------------------------------------|
| `Services/OidcLoginPhase` | Enum des étapes du flow : `Idle / Discovering / OpeningBrowser / AwaitingCallback / ExchangingCode / Success / Error` |
| `Services/YavscApiClient` | Client HTTP de l'API Yavsc. Porte `LoginInteractiveAsync(IProgress<OidcLoginPhase>)` et `TrySilentLoginAsync`. Refresh silencieux sur 401 et sur access-token bientôt expiré. |
| `Services/SingleInstance` | Named-pipe helper. `TryHandOffAsync` côté 2ᵉ instance, `StartServerAsync` côté instance vivante. |
| `Services/CustomSchemeBrowser` | `IBrowser` OidcClient qui ouvre le système + attend le pipe. |
| `Services/SchemeUrlDetector` | Détection pure, testable, du `postit://callback` dans argv. |
| `Services/TokenStore` | Persiste `RefreshTokenRecord` (access/refresh/id + expiry). Mode 0600 sur POSIX. |
| `ViewModels/SessionStatusViewModel` | VM du bandeau persistant Connecté/Déconnecté + Logout. |
| `Views/SessionStatusBanner` | Bandeau affiché en haut de `MainWindow`, visible sur toutes les pages. |
## Plateformes (`PostIt.Desktop`, `PostIt.Android`, `PostIt.Browser`)
Chaque plateforme injecte son propre `IBrowser` via
`Platform.CreateBrowser` :
- **Desktop** : `CustomSchemeBrowser` + scheme `postit://`.
- **Android** : Chrome Custom Tabs via `AndroidSystemBrowser`,
scheme `android://postit-signin` (déclaré comme
`<activity-alias>` dans `AndroidManifest.xml`). MainActivity est
`SingleTask``OnNewIntent` livre l'URL à `AndroidOidcCallbackSink`.
- **Browser (WASM)** : pas de process distinct → N/A.
## UX observable
`LoginPage` affiche en continu la **phase** courante du flow
(badge `PhaseLabel`) en plus du `StatusMessage` textuel :
| Phase | Signification |
|--------------------|------------------------------------------------------------------|
| `Discovering` | Fetch de `/.well-known/openid-configuration`. |
| `OpeningBrowser` | Navigateur système ouvert, on attend que l'utilisateur revienne. |
| `AwaitingCallback` | (idem `OpeningBrowser` aujourd'hui — couvre la fenêtre pipe). |
| `ExchangingCode` | Trade du `code` contre les tokens à `/connect/token`. |
| `Success` / `Error`| Issue du flow. |
Si le badge reste bloqué sur `OpeningBrowser`, **l'OS n'a jamais
relancé PostIt avec l'URL `postit://callback`** : le scheme
handler n'est pas enregistré correctement.
## Persistance et reprise au démarrage
Au boot, `App.OnFrameworkInitializationCompleted` :
1. Construit le `TokenStore` (~/.config/PostIt/tokens.json) et
`YavscApiClient`. Ce dernier charge `_tokens = store.Load()`
dans son constructeur.
2. Ouvre `MainWindow` avec `HomePage` comme racine.
3. Sur l'événement `Opened`, appelle `TrySilentLoginAsync` :
- si l'access token a encore `> RefreshSkew` (60 s) de vie →
`Success` immédiat, push `MainPage`.
- sinon, tente un refresh via le refresh token. Si l'IdP
accepte → push `MainPage`. Si l'IdP rejette (révocation,
vol, expiration du refresh) → `_store.Clear()`, retour à
`HomePage`.
Le bandeau `SessionStatusBanner` reflète `Api.HasValidSession`
en continu et expose le bouton **Se déconnecter**, qui appelle
`Api.LogoutAsync()` (purge du store) puis `PopToRootAsync`
pour ramener sur `HomePage`.
## Garanties testées
- `LoginInteractiveAsync` émet `Discovering → OpeningBrowser →
ExchangingCode → Success` (ou `Error` si pas de navigateur).
- `TrySilentLoginAsync` retourne `false` si pas de bundle sur
disque, `true` si l'access est encore valide, `true` après un
refresh réussi.
- `SchemeUrlDetector.FindCallbackUrl` reconnaît l'URL en argv,
case-insensitive, refuse les faux positifs (`postit-…://`),
retourne le premier match de manière déterministe.
Le chemin "refresh token rejeté → purge du store" est testé
manuellement ; le stub `OidcStubAuthority` ne sait pas encore
rejeter un refresh token précis — extension future.
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.

View file

@ -0,0 +1,79 @@
# Workflow multi-parties
> **Récapitulatif** : Yavsc orchestre des projets multi-parties
> (client, fournisseur, coordinateur) avec mise en relation,
> sous-traitance et signature de licence en amont de la production.
> Détail dans cette page, racine de l'architecture : [Architecture.md](../Architecture.md).
## Rôle de chaque partie
| Rôle | Type de compte | Description |
|---------------|--------------------|-----------------------------------------------------------|
| Client | Pro ou particulier | Exprime le besoin, valide les devis, co-signe la licence |
| Fournisseur | Pro | Répond aux besoins, peut sous-traiter |
| Coordinateur | Pro ou particulier | Orchestre le projet sans relation de facturation directe |
## Qui peut initier un projet
Le projet peut être initié par n'importe quelle partie :
- Un **client** (particulier ou pro) qui exprime un besoin
- Un **fournisseur** qui propose une offre ou monte un collectif
- Un **tiers coordinateur** qui orchestre sans être client ni prestataire
## Sous-traitance
Un fournisseur peut faire appel à d'autres fournisseurs,
**avec accord explicite du client**. Chaque sous-traitant :
- Est visible dans le projet
- Co-signe l'accord de licence
- Peut recevoir un devis distinct
## Distinction B2B / B2C
La distinction est portée par le **type de compte** :
- Compte **pro** : SIRET, TVA, facturation professionnelle
- Compte **particulier** : usage personnel, sans obligations fiscales pro
Un projet peut mélanger les deux (ex : un particulier client,
plusieurs prestataires pro) — Yavsc n'impose pas d'homogénéité.
## États d'un projet
```
Initié → En recherche de parties → Devis en cours
→ Accord de licence signé → En production
→ Livré → Publié (si licence libre)
```
## Contrainte clé
L'accord de licence est établi **avant** tout début de production,
signé (ou validé) par toutes les parties : client, fournisseurs,
et sous-traitants éventuels.
## Domaine musical — Titres collaboratifs
Permet la production collaborative de titres musicaux sous licence
libre, à partir de la mise en relation assurée par Yavsc.
### Formats supportés
- Audio (ex : WAV, FLAC, MP3)
- Partition (ex : MusicXML, LilyPond, PDF)
- MIDI
### Flux de production
1. Un client exprime un besoin musical
2. Des prestataires répondent avec des devis
3. Un **consensus est établi en amont** entre le client et les
contributeurs sur la licence du livrable final
4. La collaboration produit les fichiers
5. Le titre est publié sous la licence choisie
## Voir aussi
- [Architecture.md](../Architecture.md) — racine.