Aller au contenu

Documentation technique

Obscura Flow est une application Electron composée de trois couches:

  • main: accès système, fichiers, menus, API backend, synchronisation et logs;
  • preload: pont IPC typé entre main et renderer;
  • renderer: interface React, état UI, vues et notifications;
  • shared: types, schéma workspace, validation, i18n et helpers communs.

Le renderer ne manipule pas directement le filesystem. Toutes les opérations sensibles passent par l’API exposée dans preload/index.ts.

  1. Electron démarre le main process.
  2. SettingsService charge l’état local.
  3. WorkspaceService est prêt à créer, ouvrir ou sauvegarder les .obf.
  4. Logger écrit les événements locaux en JSONL et peut exporter vers un endpoint distant.
  5. ApiClient encapsule les appels REST vers le backend.
  6. Le preload expose window.obscuraFlow.
  7. Le renderer utilise useWorkspaceStore() pour orchestrer les actions UI.
  8. Un écran de préchargement React reste visible jusqu’au premier AppStateSnapshot, afin d’éviter un rendu noir pendant la restauration locale.
  9. Le renderer échantillonne les ressources locales via IPC toutes les 10 secondes, sans bloquer le health check API.
classDiagram
class App {
+useWorkspaceStore()
+render views
+render notifications
+render sync debug panel
+render local resource footer
}
class WorkspaceStore {
+state AppStateSnapshot
+view AppView
+refresh()
+checkApiHealth()
+createWorkspace(name, description)
+openWorkspace()
+openRecentWorkspace(filePath)
+removeRecentWorkspace(filePath)
+closeWorkspace()
+updateAppSettings(patch)
+updateWorkspaceSettings(settings)
+updateWorkspaceDetails(details)
+authenticate(credentials)
+logout()
}
class ObscuraFlowApi {
<<preload bridge>>
+getBootstrapState()
+checkApiHealth()
+getLocalResources()
+createWorkspace(params)
+openWorkspace()
+openRecentWorkspace(filePath)
+removeRecentWorkspace(filePath)
+closeWorkspace()
+showWorkspaces()
+browseDirectory(defaultPath)
+authenticate(credentials)
+logout()
+updateSettings(settings)
+updateWorkspaceSettings(settings)
+updateWorkspaceDetails(details)
+openExternalUrl(url)
+logEvent(event, details)
}
class MainProcess {
+snapshot()
+collectLocalResources()
+createWorkspace(params)
+openWorkspace()
+refreshWorkspaceFromApi()
+updateWorkspaceSettings(settings)
+updateWorkspaceDetails(details)
+syncCurrentWorkspaceWithApi(operation)
+pullWorkspaceChanges(api, clientId, token)
+drainPendingSyncOperations(api, settings)
+authenticate(credentials)
+logout()
}
class WorkspaceService {
+createDefault(params)
+createAndSave(params)
+save(workspace)
+ensureWorkspacePaths(workspace)
+open(filePath)
}
class SettingsService {
+load()
+save(settings)
+update(patch)
+addRecentWorkspace(workspace)
+removeRecentWorkspace(filePath)
}
class ApiClient {
+checkHealth()
+authenticate(credentials)
+fetchCurrentUserLicense(token)
+fetchCurrentUserWorkspaces(token)
+createWorkspace(workspace, token)
+updateWorkspace(id, workspace, token)
+loadWorkspaceById(id, token)
+syncLocalWorkspaceState(clientId, operations, token)
+fetchSyncChanges(clientId, since, token)
+acknowledgeSync(clientId, lastEventId, token)
}
class Logger {
+configureRemote(config)
+debug(event, details)
+info(event, details)
+warn(event, message, details)
+error(event, error, details)
+flushRemote()
+shutdown()
}
class LocalResourceSnapshot {
+status
+cpuPercent
+memoryMb
+memoryIncreaseMb
+freeDiskGb
+warnings
}
class ObscuraWorkspaceFile {
+schema
+schemaVersion
+exportedAt
+clientId
+workspace
+settings
+projects
+collections
+archiveHistory
+recentFiles
+metadata
}
App --> WorkspaceStore
WorkspaceStore --> ObscuraFlowApi
ObscuraFlowApi --> MainProcess
MainProcess --> WorkspaceService
MainProcess --> SettingsService
MainProcess --> ApiClient
MainProcess --> Logger
MainProcess --> LocalResourceSnapshot
WorkspaceService --> ObscuraWorkspaceFile
ApiClient --> ObscuraWorkspaceFile
Logger --> SettingsService

Le main process porte les responsabilités suivantes:

  • création de la fenêtre Electron;
  • construction du menu natif;
  • gestion IPC;
  • accès filesystem;
  • dialogue système d’ouverture de fichiers et dossiers;
  • validation et sauvegarde des workspaces;
  • synchronisation API;
  • authentification;
  • logging local et distant.
  • collecte des ressources locales du process et du volume applicatif.

Le preload expose une API strictement typée dans window.obscuraFlow.

Il isole le renderer des APIs Node/Electron directes et limite les capacités disponibles aux actions explicitement déclarées dans ObscuraFlowApi.

L’API getLocalResources() renvoie un échantillon CPU, mémoire et disque. La collecte reste dans le main process pour conserver l’isolation Node/Electron.

Le renderer est une application React.

Les vues principales sont:

  • HomeView: création et ouverture de workspace;
  • WorkspaceView: lecture du workspace courant, fermeture, édition du nom et de la description, liste des projets avec actions groupées;
  • WorkspacesView: workspaces locaux récents et workspaces distants récupérés via GET /workspaces;
  • SettingsView: expérience settings à onglets, brouillon local par catégorie, validation, sauvegarde explicite et panels spécialisés;
  • AboutView, LicenseView, DocumentationView, ContactView.

L’état est centralisé par useWorkspaceStore().

useWorkspaceStore() persiste le projet sélectionné dans localStorage sous la clé obscura:cursor:{workspaceId}. À l’ouverture d’un workspace, le curseur est restauré si le projet correspondant existe. showProject() met à jour le curseur; showView() le réinitialise quand on quitte la vue projet.

Le hook useFocusTrap(active) (dans src/renderer/components/useFocusTrap.ts) piège le focus clavier dans un conteneur tant qu’il est actif. Il est utilisé dans les modales de création de workspace, de création de projet et de modification de projet. À la désactivation, le focus est restauré sur l’élément qui l’avait avant l’ouverture de la modale.

Le footer de la sidebar affiche les ressources locales. useWorkspaceStore() met à jour ces métriques par polling léger et déclenche une notification anti-spam lorsqu’une pression CPU, mémoire ou disque est détectée.

L’application évite les écrans noirs avec trois protections:

  • couleur de fond native claire configurée sur la fenêtre Electron;
  • écran de chargement React visible tant que getBootstrapState() n’a pas retourné de snapshot;
  • collecte des ressources découplée du bootstrap et du health API.

Les seuils de surveillance locale sont définis dans src/shared/resource-monitor.ts:

  • CPU process supérieur ou égal à 80%;
  • mémoire process supérieure ou égale à 768 MB;
  • augmentation mémoire supérieure ou égale à 160 MB depuis la baseline;
  • espace disque libre inférieur ou égal à 2 GB.

Chaque dépassement produit une notification utilisateur et un log JSONL resources.local.warning.

Le schéma courant est:

schema: obscura-flow-workspace-v1
schemaVersion: 1

Le type principal est ObscuraWorkspaceFile.

Les blocs majeurs sont:

  • workspace: identité, chemins, dates, statut, version, backendId, syncCursor;
  • settings: locale, thème, chemins, structure, archive, import, refresh, sync, remote logging, remote backups, licence;
  • projects;
  • collections;
  • archiveHistory;
  • recentFiles;
  • metadata.

settings.remoteBackups contient:

  • primary et secondary: destinations de backup indépendantes;
  • checkIntervalMinutes: intervalle de contrôle automatique depuis le renderer;
  • connectionTimeoutMs: timeout appliqué aux tests de connexion;
  • credentials: champs provider-specific normalisés avec des valeurs vides par défaut.

Le renderer conserve aussi un état transitoire remoteBackupChecks dans le WorkspaceStore. Cet état n’est pas écrit dans le fichier .obf; il sert à synchroniser les derniers résultats de test entre Settings et le header.

Le test de connexion est exposé par IPC via remote-backup:check-connection. Le main process applique le timeout configuré et exécute un probe adapté au provider:

  • NAS local: lecture du dossier puis écriture/suppression d’un fichier vide;
  • NAS réseau: test TCP de l’endpoint;
  • SFTP avec clé privée: batch sftp avec ls, put, rm;
  • S3: requêtes HTTP signées AWS SigV4 PUT puis DELETE;
  • GCS: JWT service account, token OAuth, upload objet vide puis suppression;
  • Azure Blob: PUT/DELETE avec SAS token;
  • OCI Object Storage: requêtes signées OCI avec PUT puis DELETE.

Le format cible est Obscura Flow Archive Package (.ofa). Une archive ne doit plus être considérée comme un simple fichier compressé, mais comme un paquet vérifiable contenant:

  • fichier archive compressé;
  • manifest.json;
  • checksums du manifest et de l’archive;
  • signatures Ed25519;
  • integrity-package.ofi;
  • metadata.json;
  • lineage.json;
  • restore-map.json;
  • logs et rapports.

Le document de référence est Archive Package Architecture.

Le workflow cible d’archivage est transactionnel: pre-flight, discovery, integrity scan, metadata extraction, manifest, archive, checksum, signature, package .ofi, vérification locale, upload distant, vérification distante, registration et rapport final.

Les backups distants doivent uploader le package complet. Une archive dont l’intégrité ou la signature échoue bloque les actions de backup dans le renderer et dans le main process.

WorkspaceService.save() applique une sauvegarde atomique:

  1. validation complète du workspace;
  2. création des chemins nécessaires;
  3. écriture dans un fichier temporaire .tmp;
  4. renommage du fichier temporaire vers le fichier .obf.

Cette stratégie réduit le risque de fichier partiellement écrit.

La validation vérifie:

  • objet JSON racine;
  • nom de schéma attendu;
  • version de schéma supportée;
  • dates ISO;
  • UUID;
  • champs workspace obligatoires;
  • arrays attendus;
  • métadonnées minimales;
  • absence de placeholders {{...}}.

runSyncExclusive() dans src/main/main.ts garantit qu’une seule tâche de synchronisation s’exécute à la fois. Il utilise AsyncLocalStorage de node:async_hooks pour détecter la ré-entrance: si la tâche courante s’exécute déjà dans le contexte du mutex, elle est exécutée immédiatement sans mise en file. Cela permet les appels récursifs légitimes (ex: syncCurrentWorkspaceWithApi appelant runSyncExclusive depuis une tâche déjà en file) sans bloquer les IPC indépendants.

La synchronisation repose sur une version locale du workspace.

Lors d’une modification:

  1. updatedAt et exportedAt sont mis à jour;
  2. workspace.version est incrémentée;
  3. le .obf est sauvegardé;
  4. l’application tente un pull des changements distants;
  5. le workspace est envoyé en POST /workspaces ou PATCH /workspaces/{id};
  6. la réponse API met à jour backendId, serverVersion et version;
  7. les opérations en attente correspondantes sont retirées.

Si l’API est indisponible, le statut passe en offline et une opération est mise en attente.

Le health API est contrôlé périodiquement depuis le renderer. L’intervalle est lu depuis les settings globaux apiHealthCheckIntervalSeconds, avec une valeur par défaut de 30 secondes et un minimum technique de 5 secondes.

Pour le sens API vers local, le main process compare la version distante avec workspace.serverVersion. Si la version API est plus récente, le header affiche une action de refresh. Le refresh charge le workspace distant, fusionne les champs métier distants et préserve les chemins locaux (rootPath, filePath) afin de ne pas déplacer le fichier .obf.

La suite automatisée repose sur Vitest et couvre plusieurs niveaux:

  • npm run test:unit: services et helpers isolés, notamment normalisation des settings et validation de schéma;
  • npm run test:functional: règles métier observables, notamment blocage d’un dossier contenant déjà un .obf;
  • npm run test:technical: contrats d’infrastructure, notamment construction des endpoints API et parsing du health check;
  • npm run test:e2e: parcours renderer complets avec l’API preload mockée;
  • npm run test:performance: budgets d’exécution sur les chemins sensibles comme la validation workspace.

npm test exécute toutes les suites *.test.ts et *.test.tsx. npm run quality ajoute lint, format, typecheck, audit et build avant merge.

Le logger écrit un fichier JSONL local:

obscura-flow.jsonl

Il prend en charge:

  • sérialisation des écritures;
  • rotation par taille;
  • rétention locale;
  • masquage des champs sensibles;
  • file d’attente remote;
  • envoi par batch;
  • flush au shutdown.

Les clés sensibles contenant token, password, secret, authorization, license, key ou credential sont remplacées par [redacted].

La commande de packaging unsigned est:

Fenêtre de terminal
npm run package:unsigned

Elle force --publish=never pour empêcher electron-builder de tenter une publication implicite dans GitHub Actions. Les releases CI utilisent npm run package:signed:mac:all afin de produire explicitement un DMG signé x64 pour les Mac Intel et un DMG signé arm64 pour les Mac Apple Silicon.

La configuration runtime n’embarque pas d’URL API ni de configuration de remote logging codées en dur dans le bundle main. L’ordre de résolution est:

  1. variables d’environnement OBSCURA_FLOW_API_BASE_URL et OBSCURA_FLOW_REMOTE_LOGGING_*;
  2. fichier utilisateur config.json dans le dossier userData;
  3. config/local.json ou config/default.json en développement;
  4. resources/config/default.json dans l’app packagée.

Les fichiers config/default.json et config/local.json sont ignorés par Git. Le modèle committé est config/default.example.json. Les scripts de packaging exécutent scripts/prepare-runtime-config.mjs: les builds signés exigent OBSCURA_FLOW_API_BASE_URL, tandis que les builds unsigned peuvent générer une configuration locale de développement. Le bloc runtime remoteLogging alimente les nouveaux workspaces et la configuration logger par défaut tant qu’un workspace n’a pas sa propre configuration.

Le workflow de release importe un certificat Developer ID Application depuis les secrets GitHub Actions avant le packaging. Les secrets et variables utilisés sont:

  • OBSCURA_FLOW_API_BASE_URL: URL de base API injectée dans resources/config/default.json;
  • OBSCURA_FLOW_REMOTE_LOGGING_ENABLED: active l’envoi distant des logs (true, 1, yes ou on);
  • OBSCURA_FLOW_REMOTE_LOGGING_ENDPOINT_URL: endpoint HTTP d’ingestion des logs;
  • OBSCURA_FLOW_REMOTE_LOGGING_SOURCE_TOKEN: token source envoyé au service de logs;
  • OBSCURA_FLOW_REMOTE_LOGGING_BATCH_SIZE: taille de batch d’envoi;
  • OBSCURA_FLOW_REMOTE_LOGGING_FLUSH_INTERVAL_MS: intervalle de flush en millisecondes;
  • OBSCURA_FLOW_REMOTE_LOGGING_LOCAL_RETENTION_DAYS: rétention locale des logs;
  • OBSCURA_FLOW_REMOTE_LOGGING_MAX_LOG_FILE_SIZE_BYTES: taille maximale d’un fichier de log;
  • MACOS_CERTIFICATE_P12_BASE64: export .p12 encodé en base64;
  • MACOS_CERTIFICATE_PASSWORD: mot de passe de l’export .p12;
  • APPLE_ID: Apple ID du compte Developer utilisé pour la notarization;
  • APPLE_APP_SPECIFIC_PASSWORD: mot de passe d’application Apple ID pour notarytool;
  • APPLE_TEAM_ID: Team ID Apple Developer.

Le certificat attendu est Developer ID Application: Mawunam TAY (4AY78C99H2). Le workflow installe aussi l’intermédiaire Apple Developer ID Certification Authority, OU=G2, vérifie security find-identity -v -p codesigning, puis contrôle la signature du DMG et de l’app montée avec npm run verify:mac avant publication. La valeur passée à electron-builder reste sans le préfixe Developer ID Application: (Mawunam TAY (4AY78C99H2)), car electron-builder sélectionne automatiquement le type de certificat adapté.

Les entitlements Hardened Runtime sont versionnés dans build/entitlements.mac.plist et build/entitlements.mac.inherit.plist. Ils accordent uniquement les autorisations nécessaires au runtime Electron signé: JIT, mémoire exécutable non signée et désactivation de la library validation pour les binaires embarqués.

La release macOS est notarized par l’intégration @electron/notarize d’electron-builder via notarytool. La vérification CI valide le ticket stapled avec xcrun stapler validate, puis exécute spctl --assess --type execute sur l’app montée afin de détecter les builds que Gatekeeper refuserait avec le message Apple de vérification malware.

Les contrôles release complémentaires sont documentés dans Release macOS. Les limites des anciennes archives fichier unique et des providers distants sans checksum exploitable sont documentées dans Legacy Limits.

Le workflow .github/workflows/release.yml publie ensuite les fichiers release/*.dmg, release/*.blockmap et release/latest*.yml sur la release GitHub du repo source, puis crée ou met à jour la release correspondante dans ctay/obscura-flow-releases. Cette seconde publication utilise le secret PUBLIC_RELEASES_TOKEN, qui doit avoir le droit contents:write sur le dépôt public de releases. Si ce secret n’est pas configuré, la publication source reste valide et l’étape de miroir public est ignorée avec un warning GitHub Actions. Si le dépôt public est encore vide, le workflow l’initialise avec un README minimal avant de créer la release.