- 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
		
			
				
	
	
	
		
			13 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	📘 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
 
- Routes: 
- 
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)
 
- 1 pod = 1 workspace (montage: 
- 
MeiliSearch : index par workspace ( obsiviewer_<workspaceId>_notes)
Flux « Ouvrir un workspace »
- User → Hub UI → liste mes workspaces
- Click Open → Hub signe JWT (claims: sub,workspace_id,role,exp)
- Redirection vers https://obsiviewer-<id>.local/?token=...
- Pod valide JWT (clé publique Hub), vérifie workspace_id
- 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-Emailou 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.) :
{
  "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: Bearerou?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)
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)
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)
{
  "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)
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
 
- Logs récents (Hub), santé des Pods, usage disque 
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.jsonuniquement via l’API du Pod (pas d’écriture par le client).
- Écriture atomique: config.tmp+fs.rename+.bakdaté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)
 
- Importer 
- 
Aucun changement sur le modèle “1 pod = 1 voute”; tu ajoutes juste le Hub par-dessus. 
14) Exemples de docker-compose (extraits)
Hub
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)
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).