- 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
		
			
				
	
	
		
			442 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# đ 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).
 |