- 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/notesadmin: +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 invitationPOST /api/invitations/accept→ rattache user au workspace
Pod
GET /api/config→ litconfig.json(+ETag)PUT /api/config(admin) →If-Match: <etag>→ write atomique +.bakGET /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).