feat: reorganize context menu documentation into docs folder

- Moved CONTEXT_MENU_INDEX.md and CONTEXT_MENU_VERIFICATION.md into docs/ directory for better organization
- Consolidated all context menu documentation files in one location for easier maintenance
- Documentation remains complete with 1000+ lines covering implementation, integration, and verification

The change improves documentation structure by moving context menu related files into a dedicated docs folder, making it easier for developers to find an
This commit is contained in:
2025-10-25 20:17:10 -04:00
parent b873577e93
commit b1da9b111d
30 changed files with 853 additions and 369 deletions
@@ -0,0 +1,441 @@
# 📘 Document d’architecture — ObsiViewer **Hub + Pods** (Workspaces provisionnés)
> Version: 1.0 — Dernière mise à jour: aujourd’hui
> Portée: architecture complète, authentification, gestion utilisateurs/roles, workspaces, déploiement des pods, panneau d’administration, stockage des préférences, intégration avec l’image Docker **`obsiviewer-angular:latest`** (ton image actuelle).
---
## 1) Objectifs & principes
### 🎯 Objectifs
* Unifier l’accès à **plusieurs voutes** (workspaces) via un **Hub** central.
* Créer/arrêter/supprimer des **workspaces via l’UI** (sans manipuler docker-compose à la main).
* Appliquer **RBAC** (viewer/editor/admin/owner) **par workspace**.
* Isoler chaque workspace dans son **pod Docker** dédié.
* Conserver la **source de vérité** dans les fichiers Markdown/YAML + un **config partagé** dans la voute.
* Stocker les **préférences privées** (par utilisateur *et* par workspace) en DB locale du pod.
### 🚫 Non-objectifs
* Pas de Kubernetes requis (Docker Engine + Traefik suffisent).
* Pas de synchro “live” entre différentes machines distantes (mais compatible Git/CI/CD).
### 🧠 Vocabulaire
* **Hub**: service central (UI + API) qui gère identité, membres, invitations, et **provisionne des Pods** via Docker Engine.
* **Pod**: conteneur **ObsiViewer** (ton image `obsiviewer-angular:latest`) dédié à **1 workspace**.
* **Workspace**: une voute (répertoire) + ses assets + sa DB locale de préférences.
* **IdP/SSO**: Authelia/Authentik/Keycloak derrière reverse proxy.
---
## 2) Vue d’ensemble
### Composants
* **Reverse Proxy** (Traefik/Caddy/Nginx) :
* Routes: `hub.local` (Hub), `obsiviewer-*.local` (Pods)
* TLS + éventuellement SSO devant le Hub
* **Hub** (service Node/TS + SQLite) :
* UI d’admin (Workspaces, Membres, Invitations)
* **Docker orchestrator** (accès `/var/run/docker.sock`)
* Authority **JWT (RS256)** → émet tokens courts signés
* DB: `users`, `workspaces`, `memberships`, `invitations`
* **Pods** (image `obsiviewer-angular:latest`) :
* **1 pod = 1 workspace** (montage: `vault`, `assets`, `db`)
* Middleware **JWT** (valide signature Hub + `workspace_id`)
* API existantes + `/api/config` (partagé), `/api/prefs` (privé)
* DB locale SQLite `/app/db/app.sqlite` (prefs + historique)
* **MeiliSearch** : index par workspace (`obsiviewer_<workspaceId>_notes`)
### Flux « Ouvrir un workspace »
1. User → Hub UI → liste mes workspaces
2. Click **Open** → Hub signe **JWT** (claims: `sub`, `workspace_id`, `role`, `exp`)
3. Redirection vers `https://obsiviewer-<id>.local/?token=...`
4. Pod valide JWT (clé publique Hub), vérifie `workspace_id`
5. L’app charge config, prefs, index → RBAC appliqué
---
## 3) Sécurité & Authentification
### SSO recommandé
* SSO (Authelia/Authentik) **devant le Hub** via reverse proxy.
* Le proxy injecte `X-User-Id`, `X-User-Name`, `X-User-Email` **ou** forwarde un JWT.
* Le Hub **crée** le user s’il n’existe pas (id stable = `sub`).
### Émission du JWT Hub → Pods
* **Algorithme**: RS256 (clé privée côté Hub, clé publique montée dans chaque Pod)
* **TTL**: 5–15 minutes
* **Claims** (ex.) :
```json
{
"sub": "user-uuid-or-oidc-sub",
"workspace_id": "it",
"role": "editor",
"name": "Bruno Charest",
"email": "bruno@…",
"iss": "obsiviewer-hub",
"iat": 1730,
"exp": 1730
}
```
### Vérification côté Pod
* Middleware lit `Authorization: Bearer` **ou** `?token=`
* `jwt.verify(token, HUB_PUBLIC_KEY, { algorithms: ['RS256'], issuer: 'obsiviewer-hub' })`
* Vérifie `payload.workspace_id === process.env.WORKSPACE_ID`
### RBAC (par route)
* `viewer` : lecture seule (notes, assets, graph)
* `editor` : CRUD sur fichiers/dossiers/notes
* `admin` : + `PUT /api/config` (config partagée du workspace)
* `owner` (option) : tout + gestion des membres & suppression du workspace (géré plutôt au Hub)
---
## 4) Modèle de données
### Hub (SQLite ou Postgres)
```sql
CREATE TABLE users(
id TEXT PRIMARY KEY, -- sub issu du SSO
display_name TEXT,
email TEXT UNIQUE
);
CREATE TABLE workspaces(
id TEXT PRIMARY KEY, -- slug: main, it, recettes...
name TEXT,
url TEXT, -- ex: https://obsiviewer-it.local
status TEXT DEFAULT 'active' -- active/disabled
);
CREATE TABLE memberships(
user_id TEXT,
workspace_id TEXT,
role TEXT CHECK(role IN ('owner','admin','editor','viewer')),
PRIMARY KEY(user_id, workspace_id)
);
CREATE TABLE invitations(
id TEXT PRIMARY KEY, -- uuid
workspace_id TEXT,
email TEXT,
role TEXT CHECK(role IN ('admin','editor','viewer')),
token TEXT UNIQUE, -- jeton d’invitation
expires_at TEXT,
accepted_by_user_id TEXT NULL
);
```
### Pod (SQLite locale `/app/db/app.sqlite`)
```sql
CREATE TABLE IF NOT EXISTS user_prefs(
user_id TEXT,
key TEXT, -- 'ui','editor','graph','notesList', etc.
json_value TEXT,
PRIMARY KEY(user_id, key)
);
CREATE TABLE IF NOT EXISTS search_history(
user_id TEXT,
ts TEXT,
context TEXT, -- 'notes','graph',...
query TEXT
);
CREATE TABLE IF NOT EXISTS audit(
id TEXT PRIMARY KEY, ts TEXT, user_id TEXT, action TEXT, details TEXT
);
```
---
## 5) Configuration & préférences
### Partagé (par workspace, **dans la voute**)
Fichier `vault/.obsiviewer/config.json` (écriture **atomique** + `.bak` + ETag)
```json
{
"version": 1,
"ui": { "defaultTheme": "blue-light", "uiMode": "Nimbus" },
"notesList": { "density": "comfortable", "sort": "updatedAt:desc", "view": "cards" },
"graph": { "physics": { "repulsion": 2500, "linkDistance": 60 } },
"folders": { "exclusions": [".obsidian", "attachments"], "colors": { "Projects": "#9b87f5" } },
"search": { "defaults": { "caseSensitive": false, "useRegex": true } },
"quickLinks": [{ "label": "Drafts", "query": "status:draft" }]
}
```
### Privé (par user *et* workspace, **en DB du pod**)
* `user_prefs`: UI préférée, zoom/centre du graphe, onglet par défaut, options éditeur, etc.
* `search_history`: 10 dernières requêtes par contexte
### Ordre de précédence
```
Defaults app
< config.json (partagé per-workspace)
< user_prefs (privé per-user/workspace)
< device-local volatil (scroll, pliage, etc.) en localStorage
```
---
## 6) APIs
### Hub
* `GET /api/my/workspaces` → workspaces accessibles (id, name, url, role)
* `GET /api/token?workspaceId=<id>` → `{ token, url }` (JWT court)
* `POST /api/workspaces { name, slug }` → **provisionne** (crée dossiers + pod)
* `DELETE /api/workspaces/:id` → stop + remove container + purge (option)
* `POST /api/invitations` (owner/admin) → créer invitation
* `POST /api/invitations/accept` → rattache user au workspace
### Pod
* `GET /api/config` → lit `config.json` (+ `ETag`)
* `PUT /api/config` (admin) → `If-Match: <etag>` → write atomique + `.bak`
* `GET /api/prefs` → agrégat des prefs privées (clé→JSON)
* `PUT /api/prefs` → upsert clés/prefs
* **Notes CRUD** (existant) → protégé par `role >= editor`
---
## 7) Provisioning des Pods (Hub → Docker)
### Montage standard
```
/data/workspaces/<id>/vault:/app/vault
/data/workspaces/<id>/assets:/app/assets
/data/workspaces/<id>/db:/app/db
/srv/hub/keys:/keys:ro # hub_rsa.pub
```
### Variables d’environnement Pod
```
PORT=4000
NODE_ENV=production
TZ=America/Montreal
WORKSPACE_ID=<id>
REQUIRE_JWT=true
HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub
```
### Labels Traefik (ex.)
```
"traefik.enable": "true"
"traefik.http.routers.obsiviewer-<id>.rule": "Host(`obsiviewer-<id>.local`)"
"traefik.http.services.obsiviewer-<id>.loadbalancer.server.port": "4000"
```
### Exemple (Node/TS, `dockerode`)
```ts
const container = await docker.createContainer({
Image: "docker-registry.dev.home:5000/obsiviewer-angular:latest",
name: `obsiviewer-${slug}`,
Env: [
"PORT=4000",
"NODE_ENV=production",
"TZ=America/Montreal",
`WORKSPACE_ID=${slug}`,
"REQUIRE_JWT=true",
"HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub"
],
HostConfig: {
Binds: [
`/data/workspaces/${slug}/vault:/app/vault`,
`/data/workspaces/${slug}/assets:/app/assets`,
`/data/workspaces/${slug}/db:/app/db`,
`/srv/hub/keys:/keys:ro`
],
RestartPolicy: { Name: "unless-stopped" }
},
ExposedPorts: { "4000/tcp": {} },
Labels: {
"traefik.enable": "true",
[`traefik.http.routers.obsiviewer-${slug}.rule`]: `Host(\`obsiviewer-${slug}.local\`)`,
[`traefik.http.services.obsiviewer-${slug}.loadbalancer.server.port`]: "4000"
}
});
await container.start();
```
---
## 8) Reverse proxy
### Traefik — idée de base
* `hub.local` → service **hub:4100** (protégé SSO)
* `obsiviewer-*.local` → services Pods (labels dynamiques)
*(Tu peux aussi faire du DNS local type `*.local` pointant vers le reverse proxy.)*
---
## 9) Intégration **MeiliSearch**
* **1 index par workspace**: `obsiviewer_<workspaceId>_notes`
* Le Pod connaît `WORKSPACE_ID`, donc paramètre l’index au démarrage.
* Préférences de recherche (regex/case…) = côté client (user_prefs) + params de requête.
---
## 10) Panneau d’administration (Hub UI)
### Sections
* **Workspaces**
* Liste (Nom, Id, URL, Status, Membres, Dernier index)
* Actions: **Create**, **Open**, **Stop/Start**, **Delete**, **Reindex**
* **Members**
* Par workspace: lister membres (role) + **Invite** (email + rôle)
* Changer rôle / retirer membre
* **Invitations**
* Générer lien, expiration, statut
* **Settings**
* Clé publique/privée JWT (rotation)
* Paramètres Meili, SMTP (si envoi d’email), base path
* **Diagnostics**
* Logs récents (Hub), santé des Pods, usage disque `/data/workspaces`
---
## 11) Opérations, sauvegardes & reprise
### Backups
* **Vaults**: sauvegarde fichier régulière (Git + snapshots ZFS/btrfs si possible)
* **Pod DB**: sauvegarde `/data/workspaces/*/db`
* **Hub DB**: sauvegarde `/srv/hub/db`
* **Clés JWT**: `/srv/hub/keys` (privée/publique) → **sécuriser & sauvegarder**
### Reprise après incident
* Le Hub réconcilie la liste des containers via Docker API, actualise l’état en DB si besoin.
* Une suppression d’un Pod ne supprime pas automatiquement `vault/assets/db` (garde un drapeau “dangling” et propose “Purge files”).
---
## 12) Sécurité & bonnes pratiques
* Tokens courts (10 min) + rotation de clé possible (multi-keys JWKS si besoin).
* **WRITE** de `config.json` **uniquement** via l’API du Pod (pas d’écriture par le client).
* Écriture atomique: `config.tmp` + `fs.rename` + `.bak` datés.
* Ajv + **schema versionné** pour `config.json`.
* RBAC strict; endpoints sensibles (`PUT /api/config`, suppression fichier) → `role >= admin`.
---
## 13) Migration & compatibilité
* Tes Pods actuels peuvent progressivement activer `REQUIRE_JWT=false` → tests → `true`.
* Écran **“Migration des préférences locales”** (une fois) :
* Importer `ui-mode`, `graph-settings`, `folderFilterConfig`, `folderColors`, `search prefs`
* Choisir **Mes préférences** (user_prefs) vs **Défauts du workspace** (config.json, admin only)
* Aucun changement sur le modèle “1 pod = 1 voute”; tu ajoutes juste le **Hub** par-dessus.
---
## 14) Exemples de `docker-compose` (extraits)
### Hub
```yaml
services:
hub:
image: ghcr.io/tonorg/obsiviewer-hub:latest
container_name: obsiviewer-hub
environment:
- NODE_ENV=production
- PORT=4100
- TZ=America/Montreal
- HUB_JWT_PRIVATE_KEY_FILE=/keys/hub_rsa
- HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${DIR_HUB}/db:/app/db
- ${DIR_HUB}/keys:/keys:ro
ports: ["4100:4100"]
restart: unless-stopped
```
### Pod (généré par le Hub)
```yaml
services:
obsiviewer-it:
image: docker-registry.dev.home:5000/obsiviewer-angular:latest
environment:
- PORT=4000
- NODE_ENV=production
- TZ=America/Montreal
- WORKSPACE_ID=it
- REQUIRE_JWT=true
- HUB_JWT_PUBLIC_KEY_FILE=/keys/hub_rsa.pub
volumes:
- /data/workspaces/it/vault:/app/vault
- /data/workspaces/it/assets:/app/assets
- /data/workspaces/it/db:/app/db
- ${DIR_HUB}/keys:/keys:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.obsiviewer-it.rule=Host(`obsiviewer-it.local`)"
- "traefik.http.services.obsiviewer-it.loadbalancer.server.port=4000"
restart: unless-stopped
```
---
## 15) Roadmap (évolution)
* 🔒 **Liens publics** en read-only (URLs signées, expirables) par workspace
* 🔁 **Refresh token** Hub (silent refresh côté Pod, optionnel)
* 📈 **Observabilité** (Prometheus/Grafana) pour Hub + Pods
* 🧩 **Plugins** par workspace (charges dynamiques)
* 💼 **Org/Teams** au niveau Hub (groupes → rôles par défaut)
---
## 16) Résumé exécutif
* **Concept “Workspace = Voute”** conservé → isolation & simplicité.
* **Hub** ajoute: identité, membres, invitations, **provisioning automatique des Pods**, tokens JWT, panel d’administration.
* **Pods** restent proches de ton image actuelle, avec un simple **middleware JWT** + APIs `config/prefs` + RBAC.
* **Création de workspace via UI** = *instantané* (le Hub crée le dossier + lance le conteneur + publie via Traefik).
---
Si tu veux, je peux te livrer dans un message suivant :
* un **squelette Hub** (Express + dockerode + SQLite + Ajv) prêt à builder,
* le **middleware JWT+RBAC** TypeScript côté Pod,
* et un **mini guide** d’installation Traefik (labels, wildcard DNS).