@creezio/electron-shell 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/admin-window.d.ts +18 -0
- package/dist/admin-window.d.ts.map +1 -0
- package/dist/admin-window.js +75 -0
- package/dist/admin-window.js.map +1 -0
- package/dist/boot.d.ts +36 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +99 -0
- package/dist/boot.js.map +1 -0
- package/dist/desktop/assistant-chrome.d.ts +42 -0
- package/dist/desktop/assistant-chrome.d.ts.map +1 -0
- package/dist/desktop/assistant-chrome.js +178 -0
- package/dist/desktop/assistant-chrome.js.map +1 -0
- package/dist/desktop/brand-desktop-runtime.d.ts +72 -0
- package/dist/desktop/brand-desktop-runtime.d.ts.map +1 -0
- package/dist/desktop/brand-desktop-runtime.js +3926 -0
- package/dist/desktop/brand-desktop-runtime.js.map +1 -0
- package/dist/desktop/desktop-session.d.ts +85 -0
- package/dist/desktop/desktop-session.d.ts.map +1 -0
- package/dist/desktop/desktop-session.js +163 -0
- package/dist/desktop/desktop-session.js.map +1 -0
- package/dist/desktop/error-page-html.d.ts +16 -0
- package/dist/desktop/error-page-html.d.ts.map +1 -0
- package/dist/desktop/error-page-html.js +88 -0
- package/dist/desktop/error-page-html.js.map +1 -0
- package/dist/desktop/legacy-brand-compat.d.ts +45 -0
- package/dist/desktop/legacy-brand-compat.d.ts.map +1 -0
- package/dist/desktop/legacy-brand-compat.js +49 -0
- package/dist/desktop/legacy-brand-compat.js.map +1 -0
- package/dist/desktop/oauth-loopback.d.ts +28 -0
- package/dist/desktop/oauth-loopback.d.ts.map +1 -0
- package/dist/desktop/oauth-loopback.js +137 -0
- package/dist/desktop/oauth-loopback.js.map +1 -0
- package/dist/desktop/profile-picker-html.d.ts +27 -0
- package/dist/desktop/profile-picker-html.d.ts.map +1 -0
- package/dist/desktop/profile-picker-html.js +213 -0
- package/dist/desktop/profile-picker-html.js.map +1 -0
- package/dist/desktop/remote-offline-html.d.ts +14 -0
- package/dist/desktop/remote-offline-html.d.ts.map +1 -0
- package/dist/desktop/remote-offline-html.js +88 -0
- package/dist/desktop/remote-offline-html.js.map +1 -0
- package/dist/factory-reset-runtime.d.ts +11 -0
- package/dist/factory-reset-runtime.d.ts.map +1 -0
- package/dist/factory-reset-runtime.js +77 -0
- package/dist/factory-reset-runtime.js.map +1 -0
- package/dist/host/ai-workspace/actions.d.ts +12 -0
- package/dist/host/ai-workspace/actions.d.ts.map +1 -0
- package/dist/host/ai-workspace/actions.js +243 -0
- package/dist/host/ai-workspace/actions.js.map +1 -0
- package/dist/host/ai-workspace/bindings.d.ts +51 -0
- package/dist/host/ai-workspace/bindings.d.ts.map +1 -0
- package/dist/host/ai-workspace/bindings.js +40 -0
- package/dist/host/ai-workspace/bindings.js.map +1 -0
- package/dist/host/ai-workspace/index.d.ts +11 -0
- package/dist/host/ai-workspace/index.d.ts.map +1 -0
- package/dist/host/ai-workspace/index.js +6 -0
- package/dist/host/ai-workspace/index.js.map +1 -0
- package/dist/host/ai-workspace/manager.d.ts +128 -0
- package/dist/host/ai-workspace/manager.d.ts.map +1 -0
- package/dist/host/ai-workspace/manager.js +519 -0
- package/dist/host/ai-workspace/manager.js.map +1 -0
- package/dist/host/ai-workspace/profile-window.d.ts +37 -0
- package/dist/host/ai-workspace/profile-window.d.ts.map +1 -0
- package/dist/host/ai-workspace/profile-window.js +121 -0
- package/dist/host/ai-workspace/profile-window.js.map +1 -0
- package/dist/host/ai-workspace/screencast.d.ts +56 -0
- package/dist/host/ai-workspace/screencast.d.ts.map +1 -0
- package/dist/host/ai-workspace/screencast.js +245 -0
- package/dist/host/ai-workspace/screencast.js.map +1 -0
- package/dist/host/ai-workspace/types.d.ts +40 -0
- package/dist/host/ai-workspace/types.d.ts.map +1 -0
- package/dist/host/ai-workspace/types.js +6 -0
- package/dist/host/ai-workspace/types.js.map +1 -0
- package/dist/host/brand-host-runtime.d.ts +107 -0
- package/dist/host/brand-host-runtime.d.ts.map +1 -0
- package/dist/host/brand-host-runtime.js +274 -0
- package/dist/host/brand-host-runtime.js.map +1 -0
- package/dist/host/brand-host-stack.d.ts +117 -0
- package/dist/host/brand-host-stack.d.ts.map +1 -0
- package/dist/host/brand-host-stack.js +253 -0
- package/dist/host/brand-host-stack.js.map +1 -0
- package/dist/host/brand-kernel-http.d.ts +34 -0
- package/dist/host/brand-kernel-http.d.ts.map +1 -0
- package/dist/host/brand-kernel-http.js +95 -0
- package/dist/host/brand-kernel-http.js.map +1 -0
- package/dist/host/brand-meili-boot.d.ts +67 -0
- package/dist/host/brand-meili-boot.d.ts.map +1 -0
- package/dist/host/brand-meili-boot.js +100 -0
- package/dist/host/brand-meili-boot.js.map +1 -0
- package/dist/host/bridge-client.d.ts +54 -0
- package/dist/host/bridge-client.d.ts.map +1 -0
- package/dist/host/bridge-client.js +187 -0
- package/dist/host/bridge-client.js.map +1 -0
- package/dist/host/browser-tabs/browser-tab-driver.d.ts +45 -0
- package/dist/host/browser-tabs/browser-tab-driver.d.ts.map +1 -0
- package/dist/host/browser-tabs/browser-tab-driver.js +154 -0
- package/dist/host/browser-tabs/browser-tab-driver.js.map +1 -0
- package/dist/host/browser-tabs/browser-tab-manager.d.ts +208 -0
- package/dist/host/browser-tabs/browser-tab-manager.d.ts.map +1 -0
- package/dist/host/browser-tabs/browser-tab-manager.js +758 -0
- package/dist/host/browser-tabs/browser-tab-manager.js.map +1 -0
- package/dist/host/browser-tabs/browser-tab-preload-path.d.ts +2 -0
- package/dist/host/browser-tabs/browser-tab-preload-path.d.ts.map +1 -0
- package/dist/host/browser-tabs/browser-tab-preload-path.js +29 -0
- package/dist/host/browser-tabs/browser-tab-preload-path.js.map +1 -0
- package/dist/host/browser-tabs/browser-tab-preload.d.ts +11 -0
- package/dist/host/browser-tabs/browser-tab-preload.d.ts.map +1 -0
- package/dist/host/browser-tabs/browser-tab-preload.js +11 -0
- package/dist/host/browser-tabs/browser-tab-preload.js.map +1 -0
- package/dist/host/browser-tabs/chrome-ua.d.ts +19 -0
- package/dist/host/browser-tabs/chrome-ua.d.ts.map +1 -0
- package/dist/host/browser-tabs/chrome-ua.js +37 -0
- package/dist/host/browser-tabs/chrome-ua.js.map +1 -0
- package/dist/host/browser-tabs/fake-cursor-inject.d.ts +7 -0
- package/dist/host/browser-tabs/fake-cursor-inject.d.ts.map +1 -0
- package/dist/host/browser-tabs/fake-cursor-inject.js +7 -0
- package/dist/host/browser-tabs/fake-cursor-inject.js.map +1 -0
- package/dist/host/browser-tabs/index.d.ts +13 -0
- package/dist/host/browser-tabs/index.d.ts.map +1 -0
- package/dist/host/browser-tabs/index.js +13 -0
- package/dist/host/browser-tabs/index.js.map +1 -0
- package/dist/host/browser-tabs/tab-load-state.d.ts +56 -0
- package/dist/host/browser-tabs/tab-load-state.d.ts.map +1 -0
- package/dist/host/browser-tabs/tab-load-state.js +56 -0
- package/dist/host/browser-tabs/tab-load-state.js.map +1 -0
- package/dist/host/browser-tabs/tab-url.d.ts +10 -0
- package/dist/host/browser-tabs/tab-url.d.ts.map +1 -0
- package/dist/host/browser-tabs/tab-url.js +57 -0
- package/dist/host/browser-tabs/tab-url.js.map +1 -0
- package/dist/host/context.d.ts +70 -0
- package/dist/host/context.d.ts.map +1 -0
- package/dist/host/context.js +16 -0
- package/dist/host/context.js.map +1 -0
- package/dist/host/contracts.d.ts +39 -0
- package/dist/host/contracts.d.ts.map +1 -0
- package/dist/host/contracts.js +26 -0
- package/dist/host/contracts.js.map +1 -0
- package/dist/host/crash-reporter.d.ts +65 -0
- package/dist/host/crash-reporter.d.ts.map +1 -0
- package/dist/host/crash-reporter.js +371 -0
- package/dist/host/crash-reporter.js.map +1 -0
- package/dist/host/ensure-kit-binaries.d.ts +17 -0
- package/dist/host/ensure-kit-binaries.d.ts.map +1 -0
- package/dist/host/ensure-kit-binaries.js +205 -0
- package/dist/host/ensure-kit-binaries.js.map +1 -0
- package/dist/host/feature-off-host.d.ts +130 -0
- package/dist/host/feature-off-host.d.ts.map +1 -0
- package/dist/host/feature-off-host.js +138 -0
- package/dist/host/feature-off-host.js.map +1 -0
- package/dist/host/hermes/crm-key.d.ts +49 -0
- package/dist/host/hermes/crm-key.d.ts.map +1 -0
- package/dist/host/hermes/crm-key.js +126 -0
- package/dist/host/hermes/crm-key.js.map +1 -0
- package/dist/host/hermes/ensure-crm-key-db.d.ts +7 -0
- package/dist/host/hermes/ensure-crm-key-db.d.ts.map +1 -0
- package/dist/host/hermes/ensure-crm-key-db.js +84 -0
- package/dist/host/hermes/ensure-crm-key-db.js.map +1 -0
- package/dist/host/hermes/launcher.d.ts +98 -0
- package/dist/host/hermes/launcher.d.ts.map +1 -0
- package/dist/host/hermes/launcher.js +972 -0
- package/dist/host/hermes/launcher.js.map +1 -0
- package/dist/host/hermes/runtime-bootstrap.d.ts +172 -0
- package/dist/host/hermes/runtime-bootstrap.d.ts.map +1 -0
- package/dist/host/hermes/runtime-bootstrap.js +789 -0
- package/dist/host/hermes/runtime-bootstrap.js.map +1 -0
- package/dist/host/hermes/skills-seed.d.ts +40 -0
- package/dist/host/hermes/skills-seed.d.ts.map +1 -0
- package/dist/host/hermes/skills-seed.js +100 -0
- package/dist/host/hermes/skills-seed.js.map +1 -0
- package/dist/host/host-stack.d.ts +35 -0
- package/dist/host/host-stack.d.ts.map +1 -0
- package/dist/host/host-stack.js +38 -0
- package/dist/host/host-stack.js.map +1 -0
- package/dist/host/kit-os-resources.d.ts +6 -0
- package/dist/host/kit-os-resources.d.ts.map +1 -0
- package/dist/host/kit-os-resources.js +88 -0
- package/dist/host/kit-os-resources.js.map +1 -0
- package/dist/host/load-electron.d.ts +2 -0
- package/dist/host/load-electron.d.ts.map +1 -0
- package/dist/host/load-electron.js +44 -0
- package/dist/host/load-electron.js.map +1 -0
- package/dist/host/local-config.d.ts +120 -0
- package/dist/host/local-config.d.ts.map +1 -0
- package/dist/host/local-config.js +633 -0
- package/dist/host/local-config.js.map +1 -0
- package/dist/host/meili/browse.d.ts +58 -0
- package/dist/host/meili/browse.d.ts.map +1 -0
- package/dist/host/meili/browse.js +98 -0
- package/dist/host/meili/browse.js.map +1 -0
- package/dist/host/meili/coherence-db.d.ts +35 -0
- package/dist/host/meili/coherence-db.d.ts.map +1 -0
- package/dist/host/meili/coherence-db.js +129 -0
- package/dist/host/meili/coherence-db.js.map +1 -0
- package/dist/host/meili/coherence-query.d.ts +10 -0
- package/dist/host/meili/coherence-query.d.ts.map +1 -0
- package/dist/host/meili/coherence-query.js +23 -0
- package/dist/host/meili/coherence-query.js.map +1 -0
- package/dist/host/meili/coherence.d.ts +53 -0
- package/dist/host/meili/coherence.d.ts.map +1 -0
- package/dist/host/meili/coherence.js +148 -0
- package/dist/host/meili/coherence.js.map +1 -0
- package/dist/host/meili/feed.d.ts +82 -0
- package/dist/host/meili/feed.d.ts.map +1 -0
- package/dist/host/meili/feed.js +100 -0
- package/dist/host/meili/feed.js.map +1 -0
- package/dist/host/meili/generic-indexer.d.ts +34 -0
- package/dist/host/meili/generic-indexer.d.ts.map +1 -0
- package/dist/host/meili/generic-indexer.js +333 -0
- package/dist/host/meili/generic-indexer.js.map +1 -0
- package/dist/host/meili/index-schema.d.ts +64 -0
- package/dist/host/meili/index-schema.d.ts.map +1 -0
- package/dist/host/meili/index-schema.js +70 -0
- package/dist/host/meili/index-schema.js.map +1 -0
- package/dist/host/meili/index.d.ts +13 -0
- package/dist/host/meili/index.d.ts.map +1 -0
- package/dist/host/meili/index.js +8 -0
- package/dist/host/meili/index.js.map +1 -0
- package/dist/host/meili/indexer.d.ts +31 -0
- package/dist/host/meili/indexer.d.ts.map +1 -0
- package/dist/host/meili/indexer.js +49 -0
- package/dist/host/meili/indexer.js.map +1 -0
- package/dist/host/meili-launcher.d.ts +30 -0
- package/dist/host/meili-launcher.d.ts.map +1 -0
- package/dist/host/meili-launcher.js +115 -0
- package/dist/host/meili-launcher.js.map +1 -0
- package/dist/host/n8n/agent-isolation.d.ts +55 -0
- package/dist/host/n8n/agent-isolation.d.ts.map +1 -0
- package/dist/host/n8n/agent-isolation.js +158 -0
- package/dist/host/n8n/agent-isolation.js.map +1 -0
- package/dist/host/n8n/api-key.d.ts +67 -0
- package/dist/host/n8n/api-key.d.ts.map +1 -0
- package/dist/host/n8n/api-key.js +297 -0
- package/dist/host/n8n/api-key.js.map +1 -0
- package/dist/host/n8n/launcher.d.ts +126 -0
- package/dist/host/n8n/launcher.d.ts.map +1 -0
- package/dist/host/n8n/launcher.js +1170 -0
- package/dist/host/n8n/launcher.js.map +1 -0
- package/dist/host/n8n/runtime-bootstrap.d.ts +44 -0
- package/dist/host/n8n/runtime-bootstrap.d.ts.map +1 -0
- package/dist/host/n8n/runtime-bootstrap.js +230 -0
- package/dist/host/n8n/runtime-bootstrap.js.map +1 -0
- package/dist/host/node-runtime.d.ts +52 -0
- package/dist/host/node-runtime.d.ts.map +1 -0
- package/dist/host/node-runtime.js +257 -0
- package/dist/host/node-runtime.js.map +1 -0
- package/dist/host/npm-cli.d.ts +43 -0
- package/dist/host/npm-cli.d.ts.map +1 -0
- package/dist/host/npm-cli.js +213 -0
- package/dist/host/npm-cli.js.map +1 -0
- package/dist/host/plugins/accept-check.d.ts +19 -0
- package/dist/host/plugins/accept-check.d.ts.map +1 -0
- package/dist/host/plugins/accept-check.js +172 -0
- package/dist/host/plugins/accept-check.js.map +1 -0
- package/dist/host/plugins/brand-bindings.d.ts +103 -0
- package/dist/host/plugins/brand-bindings.d.ts.map +1 -0
- package/dist/host/plugins/brand-bindings.js +65 -0
- package/dist/host/plugins/brand-bindings.js.map +1 -0
- package/dist/host/plugins/control-adapters.d.ts +11 -0
- package/dist/host/plugins/control-adapters.d.ts.map +1 -0
- package/dist/host/plugins/control-adapters.js +83 -0
- package/dist/host/plugins/control-adapters.js.map +1 -0
- package/dist/host/plugins/control-extras.d.ts +61 -0
- package/dist/host/plugins/control-extras.d.ts.map +1 -0
- package/dist/host/plugins/control-extras.js +382 -0
- package/dist/host/plugins/control-extras.js.map +1 -0
- package/dist/host/plugins/control-plane.d.ts +42 -0
- package/dist/host/plugins/control-plane.d.ts.map +1 -0
- package/dist/host/plugins/control-plane.js +124 -0
- package/dist/host/plugins/control-plane.js.map +1 -0
- package/dist/host/plugins/control-token.d.ts +24 -0
- package/dist/host/plugins/control-token.d.ts.map +1 -0
- package/dist/host/plugins/control-token.js +77 -0
- package/dist/host/plugins/control-token.js.map +1 -0
- package/dist/host/plugins/crm-key.d.ts +25 -0
- package/dist/host/plugins/crm-key.d.ts.map +1 -0
- package/dist/host/plugins/crm-key.js +126 -0
- package/dist/host/plugins/crm-key.js.map +1 -0
- package/dist/host/plugins/data.d.ts +21 -0
- package/dist/host/plugins/data.d.ts.map +1 -0
- package/dist/host/plugins/data.js +100 -0
- package/dist/host/plugins/data.js.map +1 -0
- package/dist/host/plugins/events.d.ts +6 -0
- package/dist/host/plugins/events.d.ts.map +1 -0
- package/dist/host/plugins/events.js +6 -0
- package/dist/host/plugins/events.js.map +1 -0
- package/dist/host/plugins/execution-grant.d.ts +6 -0
- package/dist/host/plugins/execution-grant.d.ts.map +1 -0
- package/dist/host/plugins/execution-grant.js +6 -0
- package/dist/host/plugins/execution-grant.js.map +1 -0
- package/dist/host/plugins/git.d.ts +56 -0
- package/dist/host/plugins/git.d.ts.map +1 -0
- package/dist/host/plugins/git.js +357 -0
- package/dist/host/plugins/git.js.map +1 -0
- package/dist/host/plugins/host.d.ts +68 -0
- package/dist/host/plugins/host.d.ts.map +1 -0
- package/dist/host/plugins/host.js +276 -0
- package/dist/host/plugins/host.js.map +1 -0
- package/dist/host/plugins/launcher.d.ts +146 -0
- package/dist/host/plugins/launcher.d.ts.map +1 -0
- package/dist/host/plugins/launcher.js +489 -0
- package/dist/host/plugins/launcher.js.map +1 -0
- package/dist/host/plugins/runtime.d.ts +28 -0
- package/dist/host/plugins/runtime.d.ts.map +1 -0
- package/dist/host/plugins/runtime.js +430 -0
- package/dist/host/plugins/runtime.js.map +1 -0
- package/dist/host/plugins/test-runner.d.ts +15 -0
- package/dist/host/plugins/test-runner.d.ts.map +1 -0
- package/dist/host/plugins/test-runner.js +106 -0
- package/dist/host/plugins/test-runner.js.map +1 -0
- package/dist/host/safe-storage.d.ts +21 -0
- package/dist/host/safe-storage.d.ts.map +1 -0
- package/dist/host/safe-storage.js +55 -0
- package/dist/host/safe-storage.js.map +1 -0
- package/dist/host/sandbox/embed-sandbox.d.ts +133 -0
- package/dist/host/sandbox/embed-sandbox.d.ts.map +1 -0
- package/dist/host/sandbox/embed-sandbox.js +378 -0
- package/dist/host/sandbox/embed-sandbox.js.map +1 -0
- package/dist/host/sandbox/os-sandbox.d.ts +41 -0
- package/dist/host/sandbox/os-sandbox.d.ts.map +1 -0
- package/dist/host/sandbox/os-sandbox.js +98 -0
- package/dist/host/sandbox/os-sandbox.js.map +1 -0
- package/dist/host/server-env.d.ts +50 -0
- package/dist/host/server-env.d.ts.map +1 -0
- package/dist/host/server-env.js +64 -0
- package/dist/host/server-env.js.map +1 -0
- package/dist/host/server-launcher.d.ts +49 -0
- package/dist/host/server-launcher.d.ts.map +1 -0
- package/dist/host/server-launcher.js +43 -0
- package/dist/host/server-launcher.js.map +1 -0
- package/dist/host/tunnel/cloudflared-respawn.d.ts +58 -0
- package/dist/host/tunnel/cloudflared-respawn.d.ts.map +1 -0
- package/dist/host/tunnel/cloudflared-respawn.js +73 -0
- package/dist/host/tunnel/cloudflared-respawn.js.map +1 -0
- package/dist/host/tunnel/tunnel.d.ts +72 -0
- package/dist/host/tunnel/tunnel.d.ts.map +1 -0
- package/dist/host/tunnel/tunnel.js +462 -0
- package/dist/host/tunnel/tunnel.js.map +1 -0
- package/dist/host/web-telemetry.d.ts +16 -0
- package/dist/host/web-telemetry.d.ts.map +1 -0
- package/dist/host/web-telemetry.js +97 -0
- package/dist/host/web-telemetry.js.map +1 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +44 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +252 -0
- package/dist/logger.js.map +1 -0
- package/dist/main-facade.d.ts +51 -0
- package/dist/main-facade.d.ts.map +1 -0
- package/dist/main-facade.js +96 -0
- package/dist/main-facade.js.map +1 -0
- package/dist/meili.d.ts +19 -0
- package/dist/meili.d.ts.map +1 -0
- package/dist/meili.js +14 -0
- package/dist/meili.js.map +1 -0
- package/dist/splash-ui.d.ts +69 -0
- package/dist/splash-ui.d.ts.map +1 -0
- package/dist/splash-ui.js +431 -0
- package/dist/splash-ui.js.map +1 -0
- package/dist/tray.d.ts +44 -0
- package/dist/tray.d.ts.map +1 -0
- package/dist/tray.js +173 -0
- package/dist/tray.js.map +1 -0
- package/dist/updater.d.ts +36 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +257 -0
- package/dist/updater.js.map +1 -0
- package/dist/window-chrome.d.ts +12 -0
- package/dist/window-chrome.d.ts.map +1 -0
- package/dist/window-chrome.js +93 -0
- package/dist/window-chrome.js.map +1 -0
- package/dist-cjs/admin-window.js +116 -0
- package/dist-cjs/admin-window.js.map +1 -0
- package/dist-cjs/boot.js +139 -0
- package/dist-cjs/boot.js.map +1 -0
- package/dist-cjs/desktop/assistant-chrome.js +184 -0
- package/dist-cjs/desktop/assistant-chrome.js.map +1 -0
- package/dist-cjs/desktop/brand-desktop-runtime.js +3932 -0
- package/dist-cjs/desktop/brand-desktop-runtime.js.map +1 -0
- package/dist-cjs/desktop/desktop-session.js +171 -0
- package/dist-cjs/desktop/desktop-session.js.map +1 -0
- package/dist-cjs/desktop/error-page-html.js +92 -0
- package/dist-cjs/desktop/error-page-html.js.map +1 -0
- package/dist-cjs/desktop/legacy-brand-compat.js +56 -0
- package/dist-cjs/desktop/legacy-brand-compat.js.map +1 -0
- package/dist-cjs/desktop/oauth-loopback.js +144 -0
- package/dist-cjs/desktop/oauth-loopback.js.map +1 -0
- package/dist-cjs/desktop/profile-picker-html.js +216 -0
- package/dist-cjs/desktop/profile-picker-html.js.map +1 -0
- package/dist-cjs/desktop/remote-offline-html.js +91 -0
- package/dist-cjs/desktop/remote-offline-html.js.map +1 -0
- package/dist-cjs/factory-reset-runtime.js +117 -0
- package/dist-cjs/factory-reset-runtime.js.map +1 -0
- package/dist-cjs/host/ai-workspace/actions.js +247 -0
- package/dist-cjs/host/ai-workspace/actions.js.map +1 -0
- package/dist-cjs/host/ai-workspace/bindings.js +49 -0
- package/dist-cjs/host/ai-workspace/bindings.js.map +1 -0
- package/dist-cjs/host/ai-workspace/index.js +21 -0
- package/dist-cjs/host/ai-workspace/index.js.map +1 -0
- package/dist-cjs/host/ai-workspace/manager.js +528 -0
- package/dist-cjs/host/ai-workspace/manager.js.map +1 -0
- package/dist-cjs/host/ai-workspace/profile-window.js +125 -0
- package/dist-cjs/host/ai-workspace/profile-window.js.map +1 -0
- package/dist-cjs/host/ai-workspace/screencast.js +249 -0
- package/dist-cjs/host/ai-workspace/screencast.js.map +1 -0
- package/dist-cjs/host/ai-workspace/types.js +7 -0
- package/dist-cjs/host/ai-workspace/types.js.map +1 -0
- package/dist-cjs/host/brand-host-runtime.js +286 -0
- package/dist-cjs/host/brand-host-runtime.js.map +1 -0
- package/dist-cjs/host/brand-host-stack.js +259 -0
- package/dist-cjs/host/brand-host-stack.js.map +1 -0
- package/dist-cjs/host/brand-kernel-http.js +101 -0
- package/dist-cjs/host/brand-kernel-http.js.map +1 -0
- package/dist-cjs/host/brand-meili-boot.js +106 -0
- package/dist-cjs/host/brand-meili-boot.js.map +1 -0
- package/dist-cjs/host/bridge-client.js +191 -0
- package/dist-cjs/host/bridge-client.js.map +1 -0
- package/dist-cjs/host/browser-tabs/browser-tab-driver.js +158 -0
- package/dist-cjs/host/browser-tabs/browser-tab-driver.js.map +1 -0
- package/dist-cjs/host/browser-tabs/browser-tab-manager.js +766 -0
- package/dist-cjs/host/browser-tabs/browser-tab-manager.js.map +1 -0
- package/dist-cjs/host/browser-tabs/browser-tab-preload-path.js +35 -0
- package/dist-cjs/host/browser-tabs/browser-tab-preload-path.js.map +1 -0
- package/dist-cjs/host/browser-tabs/browser-tab-preload.js +12 -0
- package/dist-cjs/host/browser-tabs/browser-tab-preload.js.map +1 -0
- package/dist-cjs/host/browser-tabs/chrome-ua.js +41 -0
- package/dist-cjs/host/browser-tabs/chrome-ua.js.map +1 -0
- package/dist-cjs/host/browser-tabs/fake-cursor-inject.js +11 -0
- package/dist-cjs/host/browser-tabs/fake-cursor-inject.js.map +1 -0
- package/dist-cjs/host/browser-tabs/index.js +29 -0
- package/dist-cjs/host/browser-tabs/index.js.map +1 -0
- package/dist-cjs/host/browser-tabs/tab-load-state.js +59 -0
- package/dist-cjs/host/browser-tabs/tab-load-state.js.map +1 -0
- package/dist-cjs/host/browser-tabs/tab-url.js +62 -0
- package/dist-cjs/host/browser-tabs/tab-url.js.map +1 -0
- package/dist-cjs/host/context.js +20 -0
- package/dist-cjs/host/context.js.map +1 -0
- package/dist-cjs/host/contracts.js +32 -0
- package/dist-cjs/host/contracts.js.map +1 -0
- package/dist-cjs/host/crash-reporter.js +422 -0
- package/dist-cjs/host/crash-reporter.js.map +1 -0
- package/dist-cjs/host/ensure-kit-binaries.js +212 -0
- package/dist-cjs/host/ensure-kit-binaries.js.map +1 -0
- package/dist-cjs/host/feature-off-host.js +144 -0
- package/dist-cjs/host/feature-off-host.js.map +1 -0
- package/dist-cjs/host/hermes/crm-key.js +137 -0
- package/dist-cjs/host/hermes/crm-key.js.map +1 -0
- package/dist-cjs/host/hermes/ensure-crm-key-db.js +89 -0
- package/dist-cjs/host/hermes/ensure-crm-key-db.js.map +1 -0
- package/dist-cjs/host/hermes/launcher.js +981 -0
- package/dist-cjs/host/hermes/launcher.js.map +1 -0
- package/dist-cjs/host/hermes/runtime-bootstrap.js +853 -0
- package/dist-cjs/host/hermes/runtime-bootstrap.js.map +1 -0
- package/dist-cjs/host/hermes/skills-seed.js +110 -0
- package/dist-cjs/host/hermes/skills-seed.js.map +1 -0
- package/dist-cjs/host/host-stack.js +42 -0
- package/dist-cjs/host/host-stack.js.map +1 -0
- package/dist-cjs/host/kit-os-resources.js +96 -0
- package/dist-cjs/host/kit-os-resources.js.map +1 -0
- package/dist-cjs/host/load-electron.js +47 -0
- package/dist-cjs/host/load-electron.js.map +1 -0
- package/dist-cjs/host/local-config.js +640 -0
- package/dist-cjs/host/local-config.js.map +1 -0
- package/dist-cjs/host/meili/browse.js +103 -0
- package/dist-cjs/host/meili/browse.js.map +1 -0
- package/dist-cjs/host/meili/coherence-db.js +139 -0
- package/dist-cjs/host/meili/coherence-db.js.map +1 -0
- package/dist-cjs/host/meili/coherence-query.js +25 -0
- package/dist-cjs/host/meili/coherence-query.js.map +1 -0
- package/dist-cjs/host/meili/coherence.js +158 -0
- package/dist-cjs/host/meili/coherence.js.map +1 -0
- package/dist-cjs/host/meili/feed.js +108 -0
- package/dist-cjs/host/meili/feed.js.map +1 -0
- package/dist-cjs/host/meili/generic-indexer.js +340 -0
- package/dist-cjs/host/meili/generic-indexer.js.map +1 -0
- package/dist-cjs/host/meili/index-schema.js +79 -0
- package/dist-cjs/host/meili/index-schema.js.map +1 -0
- package/dist-cjs/host/meili/index.js +45 -0
- package/dist-cjs/host/meili/index.js.map +1 -0
- package/dist-cjs/host/meili/indexer.js +85 -0
- package/dist-cjs/host/meili/indexer.js.map +1 -0
- package/dist-cjs/host/meili-launcher.js +121 -0
- package/dist-cjs/host/meili-launcher.js.map +1 -0
- package/dist-cjs/host/n8n/agent-isolation.js +173 -0
- package/dist-cjs/host/n8n/agent-isolation.js.map +1 -0
- package/dist-cjs/host/n8n/api-key.js +314 -0
- package/dist-cjs/host/n8n/api-key.js.map +1 -0
- package/dist-cjs/host/n8n/launcher.js +1209 -0
- package/dist-cjs/host/n8n/launcher.js.map +1 -0
- package/dist-cjs/host/n8n/runtime-bootstrap.js +244 -0
- package/dist-cjs/host/n8n/runtime-bootstrap.js.map +1 -0
- package/dist-cjs/host/node-runtime.js +274 -0
- package/dist-cjs/host/node-runtime.js.map +1 -0
- package/dist-cjs/host/npm-cli.js +224 -0
- package/dist-cjs/host/npm-cli.js.map +1 -0
- package/dist-cjs/host/plugins/accept-check.js +176 -0
- package/dist-cjs/host/plugins/accept-check.js.map +1 -0
- package/dist-cjs/host/plugins/brand-bindings.js +78 -0
- package/dist-cjs/host/plugins/brand-bindings.js.map +1 -0
- package/dist-cjs/host/plugins/control-adapters.js +89 -0
- package/dist-cjs/host/plugins/control-adapters.js.map +1 -0
- package/dist-cjs/host/plugins/control-extras.js +397 -0
- package/dist-cjs/host/plugins/control-extras.js.map +1 -0
- package/dist-cjs/host/plugins/control-plane.js +130 -0
- package/dist-cjs/host/plugins/control-plane.js.map +1 -0
- package/dist-cjs/host/plugins/control-token.js +90 -0
- package/dist-cjs/host/plugins/control-token.js.map +1 -0
- package/dist-cjs/host/plugins/crm-key.js +135 -0
- package/dist-cjs/host/plugins/crm-key.js.map +1 -0
- package/dist-cjs/host/plugins/data.js +107 -0
- package/dist-cjs/host/plugins/data.js.map +1 -0
- package/dist-cjs/host/plugins/events.js +19 -0
- package/dist-cjs/host/plugins/events.js.map +1 -0
- package/dist-cjs/host/plugins/execution-grant.js +11 -0
- package/dist-cjs/host/plugins/execution-grant.js.map +1 -0
- package/dist-cjs/host/plugins/git.js +371 -0
- package/dist-cjs/host/plugins/git.js.map +1 -0
- package/dist-cjs/host/plugins/host.js +283 -0
- package/dist-cjs/host/plugins/host.js.map +1 -0
- package/dist-cjs/host/plugins/launcher.js +514 -0
- package/dist-cjs/host/plugins/launcher.js.map +1 -0
- package/dist-cjs/host/plugins/runtime.js +446 -0
- package/dist-cjs/host/plugins/runtime.js.map +1 -0
- package/dist-cjs/host/plugins/test-runner.js +112 -0
- package/dist-cjs/host/plugins/test-runner.js.map +1 -0
- package/dist-cjs/host/safe-storage.js +95 -0
- package/dist-cjs/host/safe-storage.js.map +1 -0
- package/dist-cjs/host/sandbox/embed-sandbox.js +396 -0
- package/dist-cjs/host/sandbox/embed-sandbox.js.map +1 -0
- package/dist-cjs/host/sandbox/os-sandbox.js +106 -0
- package/dist-cjs/host/sandbox/os-sandbox.js.map +1 -0
- package/dist-cjs/host/server-env.js +69 -0
- package/dist-cjs/host/server-env.js.map +1 -0
- package/dist-cjs/host/server-launcher.js +52 -0
- package/dist-cjs/host/server-launcher.js.map +1 -0
- package/dist-cjs/host/tunnel/cloudflared-respawn.js +80 -0
- package/dist-cjs/host/tunnel/cloudflared-respawn.js.map +1 -0
- package/dist-cjs/host/tunnel/tunnel.js +470 -0
- package/dist-cjs/host/tunnel/tunnel.js.map +1 -0
- package/dist-cjs/host/web-telemetry.js +100 -0
- package/dist-cjs/host/web-telemetry.js.map +1 -0
- package/dist-cjs/index.js +496 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/logger.js +269 -0
- package/dist-cjs/logger.js.map +1 -0
- package/dist-cjs/main-facade.js +139 -0
- package/dist-cjs/main-facade.js.map +1 -0
- package/dist-cjs/meili.js +51 -0
- package/dist-cjs/meili.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/splash-ui.js +447 -0
- package/dist-cjs/splash-ui.js.map +1 -0
- package/dist-cjs/tray.js +182 -0
- package/dist-cjs/tray.js.map +1 -0
- package/dist-cjs/updater.js +304 -0
- package/dist-cjs/updater.js.map +1 -0
- package/dist-cjs/window-chrome.js +98 -0
- package/dist-cjs/window-chrome.js.map +1 -0
- package/package.json +87 -0
- package/scripts/ensure-kit-binaries.mjs +73 -0
|
@@ -0,0 +1,3926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime desktop plateforme — extrait mécanique de tempoflow2/crm/electron/main.ts (M12).
|
|
3
|
+
* Comportement préservé ; la marque injecte deps (store, hosts, paths, vertical).
|
|
4
|
+
*
|
|
5
|
+
* P2.a — ce fichier est le MOTEUR DESKTOP PARTAGÉ, pas un runtime mort :
|
|
6
|
+
* le chemin moderne (`startBrandDesktop` → `installBrandOsDesktop`,
|
|
7
|
+
* app-runtime) l'appelle avec des adaptateurs kit ; les clients desktop
|
|
8
|
+
* legacy l'appellent directement avec leurs verticaux. H10 : la compat
|
|
9
|
+
* marque héritée (`legacy-brand-compat.ts`) a été RETIRÉE — les défauts
|
|
10
|
+
* sont génériques (`<PREFIX>_PLUGINS_DIR`, `<brandId>fid`,
|
|
11
|
+
* `<PREFIX>_API_KEY`, preload `preload.js`, `ensureDesktopNode`) et les
|
|
12
|
+
* clients legacy migrent via le codemod `scripts/codemods/H10/`. Ne pas
|
|
13
|
+
* rajouter de branche marque ici.
|
|
14
|
+
*/
|
|
15
|
+
// @ts-nocheck — types Electron/marque injectés via deps ; shim kit incomplet volontairement.
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import { closeAdminWindow, openAdminWindow } from "../admin-window.js";
|
|
21
|
+
import { initLogger, log, logError, scoped, setOpsLineHandler } from "@creezio/host-runtime";
|
|
22
|
+
import { crashLogHint, crashReportsDir } from "@creezio/host-runtime";
|
|
23
|
+
import { activateSplashStep, completeSplashStep, createRemoteSplashSteps, createSplashModel, estimateEmbedPercent, sanitizeSplashDetail, splashDataUrl, updateSplashStep, } from "../splash-ui.js";
|
|
24
|
+
import { TrayController, applyLaunchAtStartup, installCloseToTray } from "../tray.js";
|
|
25
|
+
import { checkForUpdatesNow, sendUpdateToWebContents, setUpdaterRenderer, setupAutoUpdater, } from "../updater.js";
|
|
26
|
+
import { consumeOpsLine, evaluateBootRules, getFleetSessionContext, initOpsJournal, persistBootSummary, recordFleetAction, sampleFleetActions, setFleetSessionContext, setOpsJournalHooks, track, trackDecision, trackExternal, } from "@creezio/observability";
|
|
27
|
+
import { defaultProfileForAppKind, unwrapBootProfileResult, } from "@creezio/platform-core";
|
|
28
|
+
import { isFeatureEnabled } from "@creezio/brand-config";
|
|
29
|
+
import { envForNodeScriptSpawn } from "@creezio/host-runtime";
|
|
30
|
+
import { remoteOfflineHtml } from "./remote-offline-html.js";
|
|
31
|
+
/**
|
|
32
|
+
* Installe le runtime desktop (handlers app + boot). À appeler APRÈS
|
|
33
|
+
* requestSingleInstanceLock / isolation userData marque.
|
|
34
|
+
*/
|
|
35
|
+
export function installBrandDesktopRuntime(deps) {
|
|
36
|
+
const { app, BaseWindow, WebContentsView, ipcMain, session, dialog, Menu, shell, } = deps.electron;
|
|
37
|
+
const productName = String(deps.manifest?.client?.productName || "App");
|
|
38
|
+
const productNameServer = String(deps.manifest?.server?.productName || `${productName} Server`);
|
|
39
|
+
const nodeLabel = deps.nodeRuntimeLabel || `Runtime Node ${productName}`;
|
|
40
|
+
const pluginsDirEnvKey = deps.pluginsDirEnvKey || `${deps.envPrefix}_PLUGINS_DIR`;
|
|
41
|
+
const supplierFidQueryParam = deps.supplierFidQueryParam ||
|
|
42
|
+
`${String(deps.manifest?.brandId || "app")}fid`;
|
|
43
|
+
const apiKeyEnvName = deps.apiKeyEnvName || `${deps.envPrefix}_API_KEY`;
|
|
44
|
+
const progressPrefix = `${deps.envPrefix}PROGRESS `;
|
|
45
|
+
async function syncN8nWebhookPublicUrl(onLog) {
|
|
46
|
+
const pub = deps.hosts.tunnel().publicUrlForEmbedService("n8n");
|
|
47
|
+
if (!pub)
|
|
48
|
+
return;
|
|
49
|
+
const r = await deps.hosts.n8n().applyN8nPublicBaseUrl({
|
|
50
|
+
publicBaseUrl: pub,
|
|
51
|
+
connectionMode: "local",
|
|
52
|
+
onLog: onLog || scoped("n8n"),
|
|
53
|
+
});
|
|
54
|
+
if (r)
|
|
55
|
+
n8n = r;
|
|
56
|
+
}
|
|
57
|
+
let win = null;
|
|
58
|
+
let appView = null;
|
|
59
|
+
let server = null;
|
|
60
|
+
let meili = null;
|
|
61
|
+
let hermes = null;
|
|
62
|
+
let n8n = null;
|
|
63
|
+
let bridge = null;
|
|
64
|
+
let tabs = null;
|
|
65
|
+
let aiWorkspaces = null;
|
|
66
|
+
let aiScreencaster = null;
|
|
67
|
+
let assistantChrome = null;
|
|
68
|
+
let trayController = null;
|
|
69
|
+
let quitting = false;
|
|
70
|
+
/** true une fois setupAndStart OK — avant ça, X/Alt+F4 quitte (pas close-to-tray). */
|
|
71
|
+
let bootReady = false;
|
|
72
|
+
/** Début du boot local (boîte noire : durée totale boot.done / boot.failed). */
|
|
73
|
+
let bootLocalStartedAt = 0;
|
|
74
|
+
/** true pendant un redémarrage Next (changement de clé BYOK) — ignore child-exit. */
|
|
75
|
+
let restartingServer = false;
|
|
76
|
+
/** Clés réellement injectées dans le dernier process Next (source de vérité runtime). */
|
|
77
|
+
let serverLlmActive = { openai: false, anthropic: false };
|
|
78
|
+
/** Profil actif pour cette session (local embarqué ou remote) — fixé après Continuer. */
|
|
79
|
+
let activeConnectionProfile = {
|
|
80
|
+
mode: "local",
|
|
81
|
+
localBind: "127.0.0.1",
|
|
82
|
+
chosen: false,
|
|
83
|
+
};
|
|
84
|
+
/** Base URL chargée dans la WebView CRM (127.0.0.1:port ou URL distante). */
|
|
85
|
+
let activeCrmBaseUrl = null;
|
|
86
|
+
/** Résolveur du picker de connexion (boot). */
|
|
87
|
+
let connectionChoiceResolver = null;
|
|
88
|
+
function readStoredLlmFlags() {
|
|
89
|
+
const keys = deps.store().getLlmKeys();
|
|
90
|
+
return { openai: Boolean(keys.openai), anthropic: Boolean(keys.anthropic) };
|
|
91
|
+
}
|
|
92
|
+
function syncServerLlmActiveFromStored() {
|
|
93
|
+
serverLlmActive = readStoredLlmFlags();
|
|
94
|
+
}
|
|
95
|
+
/** Splash : uniquement le progrès utile (pas les timeouts Strapi / stack traces). */
|
|
96
|
+
function isSplashProgressLine(line) {
|
|
97
|
+
const s = String(line || "").trim();
|
|
98
|
+
if (!s)
|
|
99
|
+
return false;
|
|
100
|
+
if (/strapi|api\.n8n\.io|mcp-servers|timeout of|AxiosError|deprecat|Failed to (fetch|refresh|start Python)|Database ping failed|license SDK|Task Broker|Registered runner|Building workflow|Finished building|Instance registered|Discovered \d+ cluster|Recorded version|N8N_UNVERIFIED|N8N_RUNNERS|N8N_COMPRESSION|Running n8n outside|virtual environment is missing|allowlist|GATEWAY_ALLOW|TELEGRAM_ALLOWED|No env user allowlists/i.test(s)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return /bootstrap|download|npm install|runtime (déjà|manquant)|spawn (cold|warm)|redémarrage n8n|première initialisation|données déjà présentes|réutilise n8n|déjà prêt|owner:|CLI Hermes|checksum|pip install WebUI|local_trusted|jwt=ok|Node |ready v|UI prêt|attente n8n|signal prêt|charge encore ses modules|Editor is now accessible|Installation runtime|doctor|postgres|migrations|install|venv|gateway|listening|health/i.test(s);
|
|
104
|
+
}
|
|
105
|
+
/** Ingress Cloudflare CRM + embeds (ports locaux réels après splash). */
|
|
106
|
+
async function syncTunnelIngress() {
|
|
107
|
+
if (!server)
|
|
108
|
+
return;
|
|
109
|
+
await deps.hosts.tunnel().configureTunnelIngress({
|
|
110
|
+
crmPort: server.port,
|
|
111
|
+
n8nPort: deps.vertical.portFromLocalUrl(n8n?.uiUrl),
|
|
112
|
+
hermesPort: deps.vertical.portFromLocalUrl(hermes?.webuiUrl),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function remoteStatusOpts() {
|
|
116
|
+
return { remoteCrmOrigin: activeConnectionProfile.remoteUrl ?? null };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Statuts hermes/n8n côté CLIENT léger : mêmes formes de payload que les
|
|
120
|
+
* launchers, calculées SANS les modules host-only (exclus du paquet Client).
|
|
121
|
+
* Un client est toujours en mode remote → URLs dérivées du profil rejoint.
|
|
122
|
+
*/
|
|
123
|
+
function clientHermesStatusPayload() {
|
|
124
|
+
const pub = deps.vertical.hermesPublicStatus({
|
|
125
|
+
connectionMode: "remote",
|
|
126
|
+
config: deps.store().getHermesEmbedConfig(),
|
|
127
|
+
binaryFound: false,
|
|
128
|
+
running: false,
|
|
129
|
+
apiUrl: null,
|
|
130
|
+
lastError: null,
|
|
131
|
+
version: null,
|
|
132
|
+
remoteCrmOrigin: activeConnectionProfile.remoteUrl ?? null,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
...pub,
|
|
136
|
+
webuiStatus: pub.webuiUrl ? "running" : "skipped",
|
|
137
|
+
binaryPath: null,
|
|
138
|
+
homeDir: null,
|
|
139
|
+
bootstrapPhase: "idle",
|
|
140
|
+
bootstrapError: null,
|
|
141
|
+
installing: false,
|
|
142
|
+
logs: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function clientN8nStatusPayload() {
|
|
146
|
+
const pub = deps.vertical.n8nPublicStatus({
|
|
147
|
+
connectionMode: "remote",
|
|
148
|
+
config: deps.store().getN8nEmbedConfig(),
|
|
149
|
+
entryFound: false,
|
|
150
|
+
running: false,
|
|
151
|
+
uiUrl: null,
|
|
152
|
+
lastError: null,
|
|
153
|
+
version: null,
|
|
154
|
+
remoteCrmOrigin: activeConnectionProfile.remoteUrl ?? null,
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
...pub,
|
|
158
|
+
entryPath: null,
|
|
159
|
+
homeDir: null,
|
|
160
|
+
bootstrapPhase: "idle",
|
|
161
|
+
bootstrapError: null,
|
|
162
|
+
installing: false,
|
|
163
|
+
ownerReady: false,
|
|
164
|
+
logs: [],
|
|
165
|
+
localUiUrl: null,
|
|
166
|
+
publicWebhookUrl: null,
|
|
167
|
+
listenHost: "127.0.0.1",
|
|
168
|
+
listenPort: 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function llmStatusPayload() {
|
|
172
|
+
const stored = readStoredLlmFlags();
|
|
173
|
+
return {
|
|
174
|
+
openai: stored.openai,
|
|
175
|
+
anthropic: stored.anthropic,
|
|
176
|
+
serverOpenAi: serverLlmActive.openai,
|
|
177
|
+
serverAnthropic: serverLlmActive.anthropic,
|
|
178
|
+
assistantReady: serverLlmActive.openai,
|
|
179
|
+
restarting: restartingServer,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function emitLlmStatusChanged() {
|
|
183
|
+
try {
|
|
184
|
+
if (appView && !appView.webContents.isDestroyed()) {
|
|
185
|
+
appView.webContents.send("config:llm-status-changed", llmStatusPayload());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
logError("ipc", e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function attachNextExitHandler(childServer) {
|
|
193
|
+
childServer.child.on("exit", (code) => {
|
|
194
|
+
log("next", `serveur terminé (code ${code})`);
|
|
195
|
+
if (quitting || restartingServer)
|
|
196
|
+
return;
|
|
197
|
+
if (code !== 0 && code !== null) {
|
|
198
|
+
deps.vertical.reportCrash("child-exit", { child: "next-server", code });
|
|
199
|
+
if (appView && !appView.webContents.isDestroyed()) {
|
|
200
|
+
void appView.webContents.loadURL(errorHtml("Le serveur local s'est arrêté", `Le serveur interne de ${productName} s'est arrêté de façon inattendue (code ${code}).`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Redémarre le process Next pour réinjecter AUTH_* / BYOK (env figée au spawn).
|
|
207
|
+
* Préserve le port si possible → tunnel Cloudflare inchangé.
|
|
208
|
+
*/
|
|
209
|
+
function currentBindHost() {
|
|
210
|
+
return activeConnectionProfile.localBind === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1";
|
|
211
|
+
}
|
|
212
|
+
function crmBaseUrl() {
|
|
213
|
+
return activeCrmBaseUrl || server?.baseUrl || null;
|
|
214
|
+
}
|
|
215
|
+
async function restartNextServer(opts = {}) {
|
|
216
|
+
if (activeConnectionProfile.mode === "remote") {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: "Mode serveur distant : pas de serveur local à redémarrer",
|
|
220
|
+
status: llmStatusPayload(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (!server) {
|
|
224
|
+
syncServerLlmActiveFromStored();
|
|
225
|
+
emitLlmStatusChanged();
|
|
226
|
+
return { ok: true, status: llmStatusPayload() };
|
|
227
|
+
}
|
|
228
|
+
if (restartingServer) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: "Redémarrage du serveur déjà en cours",
|
|
232
|
+
status: llmStatusPayload(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
restartingServer = true;
|
|
236
|
+
emitLlmStatusChanged();
|
|
237
|
+
const prevPort = server.port;
|
|
238
|
+
const prevBase = server.baseUrl;
|
|
239
|
+
try {
|
|
240
|
+
bridge?.stop();
|
|
241
|
+
bridge = null;
|
|
242
|
+
const old = server;
|
|
243
|
+
server = null;
|
|
244
|
+
old.stop();
|
|
245
|
+
// Laisser le port TCP se libérer (surtout sous Windows / Defender).
|
|
246
|
+
for (let i = 0; i < 20; i++) {
|
|
247
|
+
try {
|
|
248
|
+
const free = await deps.hosts.server().findFreePort(prevPort);
|
|
249
|
+
if (free === prevPort)
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
/* retry */
|
|
254
|
+
}
|
|
255
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
256
|
+
}
|
|
257
|
+
const publicUrl = deps.hosts.tunnel().publicUrlForServer();
|
|
258
|
+
// Préférer l'ancien port pour ne pas casser le tunnel / cookies.
|
|
259
|
+
process.env[`${deps.envPrefix}_DESKTOP_PORT`] = String(prevPort);
|
|
260
|
+
server = await deps.hosts.server().startNextServer({
|
|
261
|
+
meiliHost: meili?.host ?? null,
|
|
262
|
+
meiliMasterKey: meili?.masterKey ?? null,
|
|
263
|
+
bindHost: currentBindHost(),
|
|
264
|
+
extraEnv: {
|
|
265
|
+
...(publicUrl
|
|
266
|
+
? { APP_PUBLIC_URL: publicUrl, MCP_PUBLIC_URL: publicUrl }
|
|
267
|
+
: {}),
|
|
268
|
+
...deps.hosts.hermes().getHermesNextEnv(activeConnectionProfile.mode),
|
|
269
|
+
...deps.hosts.n8n().getN8nNextEnv(activeConnectionProfile.mode),
|
|
270
|
+
...deps.store().getEmailNextEnv(),
|
|
271
|
+
// Onglets Données / n8n du Product Hub côté Next (pluginDataPath).
|
|
272
|
+
[pluginsDirEnvKey]: deps.hosts.pluginRuntime().pluginsRootDir(),
|
|
273
|
+
},
|
|
274
|
+
onLog: scoped("next"),
|
|
275
|
+
});
|
|
276
|
+
attachNextExitHandler(server);
|
|
277
|
+
activeCrmBaseUrl = server.baseUrl;
|
|
278
|
+
syncServerLlmActiveFromStored();
|
|
279
|
+
log("main", `serveur Next relancé sur ${server.baseUrl} (bind ${currentBindHost()}, BYOK openai=${serverLlmActive.openai} anthropic=${serverLlmActive.anthropic})`);
|
|
280
|
+
if (publicUrl && server.port !== prevPort) {
|
|
281
|
+
try {
|
|
282
|
+
await syncTunnelIngress();
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
logError("tunnel", e);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
await startBridgeIfReady();
|
|
289
|
+
const doLogin = opts.forceAutoLogin || deps.store().shouldAutoLoginOnBoot();
|
|
290
|
+
if (doLogin) {
|
|
291
|
+
try {
|
|
292
|
+
await autoLogin(server.baseUrl);
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
logError("auto-login", e);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const target = opts.navigateTo != null
|
|
299
|
+
? `${server.baseUrl}${opts.navigateTo.startsWith("/") ? opts.navigateTo : `/${opts.navigateTo}`}`
|
|
300
|
+
: server.baseUrl;
|
|
301
|
+
const shouldReload = Boolean(opts.reload || opts.navigateTo) || server.baseUrl !== prevBase;
|
|
302
|
+
if (appView && !appView.webContents.isDestroyed() && shouldReload) {
|
|
303
|
+
await appView.webContents.loadURL(target).catch(() => { });
|
|
304
|
+
}
|
|
305
|
+
return { ok: true, status: llmStatusPayload() };
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
309
|
+
logError("main", e);
|
|
310
|
+
deps.vertical.reportCrash("boot-failure", {
|
|
311
|
+
step: "next-restart",
|
|
312
|
+
message: message.slice(0, 500),
|
|
313
|
+
});
|
|
314
|
+
return { ok: false, error: message, status: llmStatusPayload() };
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
restartingServer = false;
|
|
318
|
+
emitLlmStatusChanged();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function restartNextServerForLlm() {
|
|
322
|
+
return restartNextServer({ reload: false });
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Réinjecte HERMES_* dans Next après un spawn Hermes réussi.
|
|
326
|
+
* No-op au boot (Next pas encore up) — évite le double restart.
|
|
327
|
+
* Delta M12p (Certivan) porté plateforme.
|
|
328
|
+
*/
|
|
329
|
+
async function maybeRestartNextAfterHermesSpawn(hermesSpawned) {
|
|
330
|
+
const should = typeof deps.vertical.shouldRestartNextAfterHermesStart === "function"
|
|
331
|
+
? deps.vertical.shouldRestartNextAfterHermesStart({
|
|
332
|
+
hermesSpawned,
|
|
333
|
+
nextServerRunning: Boolean(server),
|
|
334
|
+
})
|
|
335
|
+
: Boolean(hermesSpawned) && Boolean(server);
|
|
336
|
+
if (!should)
|
|
337
|
+
return;
|
|
338
|
+
log("main", "Hermes (re)spawné alors que Next tourne — réinjection HERMES_*…");
|
|
339
|
+
const r = await restartNextServer({ reload: false });
|
|
340
|
+
if (!r.ok) {
|
|
341
|
+
logError("main", new Error(r.error || "redémarrage Next après Hermes échoué"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** Exécuteur bridge partagé local/remote (workspaces IA + onglets fournisseurs). */
|
|
345
|
+
function bridgeExecutor() {
|
|
346
|
+
return async (req) => {
|
|
347
|
+
if (deps.vertical.isAiWorkspaceActionType(req.type) && aiWorkspaces) {
|
|
348
|
+
return deps.vertical.executeAiWorkspaceAction(aiWorkspaces, req, aiScreencaster ?? undefined);
|
|
349
|
+
}
|
|
350
|
+
// Actions supplier « classiques » : si un espace IA est affiché,
|
|
351
|
+
// cibler son TabManager (sinon owner).
|
|
352
|
+
const activeAi = aiWorkspaces?.getActiveUserId();
|
|
353
|
+
const activeTabs = activeAi && aiWorkspaces
|
|
354
|
+
? aiWorkspaces.getTabs(activeAi)
|
|
355
|
+
: null;
|
|
356
|
+
const targetTabs = activeTabs || tabs;
|
|
357
|
+
const targetView = activeAi && aiWorkspaces
|
|
358
|
+
? aiWorkspaces.getView(activeAi)
|
|
359
|
+
: appView;
|
|
360
|
+
return deps.vertical.executeSupplierAction(targetTabs, req, {
|
|
361
|
+
onTabOpened: (info) => {
|
|
362
|
+
if (targetView)
|
|
363
|
+
emitSupplierTabOpened(targetView, info);
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/** Screencaster IA posté via la session du bridge courant. */
|
|
369
|
+
function attachAiScreencaster() {
|
|
370
|
+
if (!aiWorkspaces)
|
|
371
|
+
return;
|
|
372
|
+
aiScreencaster = deps.vertical.createAiScreencaster({
|
|
373
|
+
manager: aiWorkspaces,
|
|
374
|
+
postFrame: async (payload) => bridge
|
|
375
|
+
? bridge.postJson("/api/v1/desktop/screencast/frame", payload)
|
|
376
|
+
: null,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async function startBridgeIfReady() {
|
|
380
|
+
if (!server)
|
|
381
|
+
return;
|
|
382
|
+
const auth = deps.store().getLocalAuth();
|
|
383
|
+
if (!auth)
|
|
384
|
+
return;
|
|
385
|
+
try {
|
|
386
|
+
bridge?.stop();
|
|
387
|
+
aiScreencaster?.stopAll();
|
|
388
|
+
bridge = deps.vertical.createBridgeClient({
|
|
389
|
+
baseUrl: server.baseUrl,
|
|
390
|
+
authUser: auth.authUser,
|
|
391
|
+
authPassword: auth.authPassword,
|
|
392
|
+
sessionCookieName: deps.sessionCookieName,
|
|
393
|
+
executor: bridgeExecutor(),
|
|
394
|
+
onLog: scoped("bridge"),
|
|
395
|
+
});
|
|
396
|
+
await bridge.start();
|
|
397
|
+
// Vue live : capture CDP des espaces IA, frames POSTées via la session
|
|
398
|
+
// bridge (start/stop pilotés par le serveur au 1er/dernier spectateur).
|
|
399
|
+
attachAiScreencaster();
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
logError("bridge", e);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Bridge computer-use en mode REMOTE (client thin) : auth « session » —
|
|
407
|
+
* réutilise le cookie CRM posé par le login UI dans la partition appView.
|
|
408
|
+
* Tant que l'utilisateur n'est pas connecté, le loop du bridge retente avec
|
|
409
|
+
* backoff (aucun credential local sur le poste client).
|
|
410
|
+
*/
|
|
411
|
+
async function startRemoteBridge(baseUrl) {
|
|
412
|
+
try {
|
|
413
|
+
bridge?.stop();
|
|
414
|
+
aiScreencaster?.stopAll();
|
|
415
|
+
const cookieName = deps.sessionCookieName;
|
|
416
|
+
bridge = deps.vertical.createBridgeClient({
|
|
417
|
+
baseUrl,
|
|
418
|
+
sessionCookieName: cookieName,
|
|
419
|
+
getSessionCookie: async () => {
|
|
420
|
+
try {
|
|
421
|
+
const ses = session.fromPartition(deps.sessionPartition);
|
|
422
|
+
const cookies = await ses.cookies.get({
|
|
423
|
+
url: baseUrl,
|
|
424
|
+
name: cookieName,
|
|
425
|
+
});
|
|
426
|
+
const c = cookies?.[0];
|
|
427
|
+
return c ? `${cookieName}=${c.value}` : null;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
deviceId: `desktop-${os.hostname() || "client"}`,
|
|
434
|
+
deviceLabel: `${productName} (${os.hostname() || "poste"})`,
|
|
435
|
+
executor: bridgeExecutor(),
|
|
436
|
+
onLog: scoped("bridge"),
|
|
437
|
+
});
|
|
438
|
+
await bridge.start();
|
|
439
|
+
attachAiScreencaster();
|
|
440
|
+
log("bridge", `bridge remote démarré vers ${baseUrl} (auth session)`);
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
logError("bridge", e);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/** Efface le cookie de session CRM uniquement (pas les partitions fournisseurs). */
|
|
447
|
+
async function clearCrmSessionCookie(baseUrl) {
|
|
448
|
+
const ses = session.fromPartition(deps.sessionPartition);
|
|
449
|
+
try {
|
|
450
|
+
await ses.cookies.remove(baseUrl, deps.sessionCookieName);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
/* ignore */
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** Réaffiche la fenêtre principale (masquée dans le tray, minimisée…). */
|
|
457
|
+
function showMainWindow() {
|
|
458
|
+
if (!win || win.isDestroyed())
|
|
459
|
+
return;
|
|
460
|
+
if (win.isMinimized())
|
|
461
|
+
win.restore();
|
|
462
|
+
win.show();
|
|
463
|
+
win.focus();
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Relance le même exécutable en instance détachée pour un lancement de profil
|
|
467
|
+
* (join/ai) arrivé sur cette instance : le nouveau process prendra son propre
|
|
468
|
+
* userData → son propre lock (aucun conflit avec l'instance courante).
|
|
469
|
+
*/
|
|
470
|
+
function spawnProfileInstance(commandLine) {
|
|
471
|
+
const profileArgs = commandLine.filter((a) => a.startsWith(`--${deps.profileArgPrefix}-profile`) || a.toLowerCase().startsWith(`${deps.deepLinkProtocol}://`));
|
|
472
|
+
if (!profileArgs.length)
|
|
473
|
+
return;
|
|
474
|
+
const args = app.isPackaged ? profileArgs : [app.getAppPath(), ...profileArgs];
|
|
475
|
+
try {
|
|
476
|
+
spawn(process.execPath, args, { detached: true, stdio: "ignore" }).unref();
|
|
477
|
+
log("profile", `relance instance profil : ${profileArgs.join(" ")}`);
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
logError("profile", e);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
app.on("second-instance", (_event, commandLine) => {
|
|
484
|
+
// Ne pas avaler un lancement de profil : le déléguer à sa propre instance.
|
|
485
|
+
// App Serveur : jamais de profil client — double-clic = refocus fenêtre.
|
|
486
|
+
const launch = deps.vertical.parseProfileArgv(commandLine || []);
|
|
487
|
+
if (deps.appKind !== "server" && launch.mode !== "server") {
|
|
488
|
+
spawnProfileInstance(commandLine || []);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (win && !win.isDestroyed()) {
|
|
492
|
+
showMainWindow();
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
/*
|
|
496
|
+
* Deep-link `tempoflow://join/<host>` — lien d'invitation collaborateur humain.
|
|
497
|
+
* Enregistrement du protocole : packagé → simple ; dev → binaire electron +
|
|
498
|
+
* chemin de l'app (pattern officiel Electron).
|
|
499
|
+
*/
|
|
500
|
+
try {
|
|
501
|
+
// Le deep-link join appartient à l'app CLIENT (et au legacy) : l'app
|
|
502
|
+
// Serveur ne s'enregistre pas sur tempoflow:// (installables côte à côte).
|
|
503
|
+
if (deps.bootBehavior.registerDeepLink) {
|
|
504
|
+
if (process.defaultApp) {
|
|
505
|
+
if (process.argv.length >= 2) {
|
|
506
|
+
app.setAsDefaultProtocolClient(deps.deepLinkProtocol, process.execPath, [
|
|
507
|
+
path.resolve(process.argv[1]),
|
|
508
|
+
]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
app.setAsDefaultProtocolClient(deps.deepLinkProtocol);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
logError("profile", e);
|
|
518
|
+
}
|
|
519
|
+
// macOS : le deep-link arrive via open-url sur l'instance en cours →
|
|
520
|
+
// déléguer au profil join dédié (Windows/Linux passent par argv/second-instance).
|
|
521
|
+
app.on("open-url", (event, url) => {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
if (deps.appKind === "server")
|
|
524
|
+
return; // l'app Serveur ignore les deep-links join
|
|
525
|
+
const target = deps.vertical.parseJoinDeepLink(url);
|
|
526
|
+
if (!target)
|
|
527
|
+
return;
|
|
528
|
+
if (deps.bootProfileLaunch?.mode === "join" &&
|
|
529
|
+
deps.bootProfileLaunch?.serverUrl === target) {
|
|
530
|
+
showMainWindow();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const joinArg = deps.vertical.profileArgFor({ mode: "join", serverUrl: target });
|
|
534
|
+
if (joinArg)
|
|
535
|
+
spawnProfileInstance([joinArg]);
|
|
536
|
+
});
|
|
537
|
+
/* ────────────────────────── Splash / progression ───────────────────────── */
|
|
538
|
+
/** Écran multi-lignes : étapes, barres, chronos (SoT kit splash-ui). */
|
|
539
|
+
function splashHtmlUrl() {
|
|
540
|
+
return splashDataUrl({
|
|
541
|
+
productName: deps.manifest.client.productName,
|
|
542
|
+
bridgeName: deps.bridgeName,
|
|
543
|
+
windowChrome: process.platform === "win32",
|
|
544
|
+
accentColor: deps.accentColor,
|
|
545
|
+
cssPrefix: deps.cssPrefix,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
let splashModel = null;
|
|
549
|
+
let lastSplashPush = 0;
|
|
550
|
+
function pushSplashModel(force = false) {
|
|
551
|
+
if (!splashModel || !appView)
|
|
552
|
+
return;
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
if (!force && now - lastSplashPush < 120)
|
|
555
|
+
return;
|
|
556
|
+
lastSplashPush = now;
|
|
557
|
+
appView.webContents
|
|
558
|
+
.executeJavaScript(`window.__setBoot && window.__setBoot(${JSON.stringify(splashModel)})`)
|
|
559
|
+
.catch(() => { });
|
|
560
|
+
}
|
|
561
|
+
function splashBeginLocal(opts) {
|
|
562
|
+
splashModel = createSplashModel(deps.createLocalSplashSteps(opts), `Préparation de la stack ${deps.manifest.client.productName}…`);
|
|
563
|
+
pushSplashModel(true);
|
|
564
|
+
}
|
|
565
|
+
function splashBeginRemote() {
|
|
566
|
+
splashModel = createSplashModel(createRemoteSplashSteps(), "Connexion au serveur distant…");
|
|
567
|
+
splashModel.footer =
|
|
568
|
+
"Mode Rejoindre — la stack (Hermes / n8n) tourne sur l’hôte.";
|
|
569
|
+
pushSplashModel(true);
|
|
570
|
+
}
|
|
571
|
+
function splashGo(id, opts) {
|
|
572
|
+
if (!splashModel)
|
|
573
|
+
return;
|
|
574
|
+
splashModel = activateSplashStep(splashModel, id, opts);
|
|
575
|
+
pushSplashModel(true);
|
|
576
|
+
}
|
|
577
|
+
function splashPatch(id, patch) {
|
|
578
|
+
if (!splashModel)
|
|
579
|
+
return;
|
|
580
|
+
splashModel = updateSplashStep(splashModel, id, patch);
|
|
581
|
+
pushSplashModel();
|
|
582
|
+
}
|
|
583
|
+
function splashDone(id, detail = "Terminé") {
|
|
584
|
+
if (!splashModel)
|
|
585
|
+
return;
|
|
586
|
+
splashModel = completeSplashStep(splashModel, id, detail);
|
|
587
|
+
pushSplashModel(true);
|
|
588
|
+
}
|
|
589
|
+
function splashEmbedLine(id, line) {
|
|
590
|
+
const detail = sanitizeSplashDetail(line);
|
|
591
|
+
if (!detail)
|
|
592
|
+
return;
|
|
593
|
+
splashPatch(id, {
|
|
594
|
+
detail,
|
|
595
|
+
percent: estimateEmbedPercent(detail),
|
|
596
|
+
status: "running",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// L'écran de profils (ex-connectionPickerHtml) vit dans profile-picker-html.ts.
|
|
600
|
+
function errorHtml(title, message) {
|
|
601
|
+
return deps.vertical.errorPageDataUrl(title, message);
|
|
602
|
+
}
|
|
603
|
+
function setSplashStatus(text) {
|
|
604
|
+
if (splashModel) {
|
|
605
|
+
splashModel = { ...splashModel, headline: text };
|
|
606
|
+
pushSplashModel();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
appView?.webContents
|
|
610
|
+
.executeJavaScript(`window.__setStatus && window.__setStatus(${JSON.stringify(text)})`)
|
|
611
|
+
.catch(() => { });
|
|
612
|
+
}
|
|
613
|
+
let lastProgressLog = 0;
|
|
614
|
+
function setSplashProgress(p) {
|
|
615
|
+
// Trace throttlée dans le log persistant (diagnostic support à distance).
|
|
616
|
+
if (p && Date.now() - lastProgressLog > 2000) {
|
|
617
|
+
lastProgressLog = Date.now();
|
|
618
|
+
log("setup", `${p.phase} ${p.percent?.toFixed(1) ?? "?"}% — ${p.detail ?? ""}`);
|
|
619
|
+
}
|
|
620
|
+
if (splashModel && p) {
|
|
621
|
+
const id = p.phase === "index" ? "index" : "catalog";
|
|
622
|
+
const detail = sanitizeSplashDetail(p.detail || "");
|
|
623
|
+
splashPatch(id, {
|
|
624
|
+
status: "running",
|
|
625
|
+
percent: typeof p.percent === "number" ? p.percent : null,
|
|
626
|
+
detail: detail ||
|
|
627
|
+
(typeof p.percent === "number"
|
|
628
|
+
? `${p.percent.toFixed(0)} %`
|
|
629
|
+
: "En cours…"),
|
|
630
|
+
headline: id === "index"
|
|
631
|
+
? "Indexation du catalogue…"
|
|
632
|
+
: p.phase === "verify"
|
|
633
|
+
? "Vérification du catalogue…"
|
|
634
|
+
: p.phase === "decompress"
|
|
635
|
+
? "Installation du catalogue…"
|
|
636
|
+
: "Téléchargement du catalogue…",
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
appView?.webContents
|
|
641
|
+
.executeJavaScript(`window.__setProgress && window.__setProgress(${JSON.stringify(p ? { percent: p.percent, detail: p.detail || "" } : { percent: null, detail: "" })})`)
|
|
642
|
+
.catch(() => { });
|
|
643
|
+
}
|
|
644
|
+
/* ─────────────────────── Sous-process Node vanilla ─────────────────────── */
|
|
645
|
+
/**
|
|
646
|
+
* Migrations SQLite dans un process Node vanilla (ABI better-sqlite3).
|
|
647
|
+
* Retourne la voie réellement empruntée pour un jalon splash honnête.
|
|
648
|
+
*/
|
|
649
|
+
function runMigrationsInNode() {
|
|
650
|
+
const script = deps.paths.nodeScript(path.join("migrations", "runner.js"));
|
|
651
|
+
// Marques native-kernel (TF3 / factory) : migrations core/brand déjà
|
|
652
|
+
// appliquées in-process par createBrandKernel — le runner.js legacy TF2
|
|
653
|
+
// n'existe pas. Ce n'est pas un « skip » : la base est bien à jour.
|
|
654
|
+
if (!fs.existsSync(script)) {
|
|
655
|
+
log("migrate", "migrations kernel appliquées in-process (pas de runner.js legacy)");
|
|
656
|
+
return Promise.resolve("kernel");
|
|
657
|
+
}
|
|
658
|
+
return new Promise((resolve, reject) => {
|
|
659
|
+
const bin = deps.paths.nodeBinary();
|
|
660
|
+
const env = envForNodeScriptSpawn(bin);
|
|
661
|
+
const nm = deps.paths.nodeModulesPathForScripts();
|
|
662
|
+
if (nm)
|
|
663
|
+
env.NODE_PATH = nm;
|
|
664
|
+
// Packagé : resources/seeds ; aussi exposé via TF2_RESOURCES_PATH.
|
|
665
|
+
if (process.resourcesPath) {
|
|
666
|
+
env[`${deps.envPrefix}_RESOURCES_PATH`] = process.resourcesPath;
|
|
667
|
+
const seeds = path.join(process.resourcesPath, "seeds");
|
|
668
|
+
if (!env[`${deps.envPrefix}_SEEDS_DIR`])
|
|
669
|
+
env[`${deps.envPrefix}_SEEDS_DIR`] = seeds;
|
|
670
|
+
}
|
|
671
|
+
log("migrate", `spawn ${bin} ${script}${env.ELECTRON_RUN_AS_NODE ? " (ELECTRON_RUN_AS_NODE)" : ""}`);
|
|
672
|
+
const child = spawn(bin, [script, deps.paths.dbPath()], {
|
|
673
|
+
env,
|
|
674
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
675
|
+
windowsHide: true,
|
|
676
|
+
});
|
|
677
|
+
const stderrTail = [];
|
|
678
|
+
child.stdout?.on("data", (d) => {
|
|
679
|
+
for (const lineRaw of d.toString().split("\n")) {
|
|
680
|
+
const s = lineRaw.trim();
|
|
681
|
+
if (!s)
|
|
682
|
+
continue;
|
|
683
|
+
if (!consumeOpsLine("migrations", s))
|
|
684
|
+
log("migrate", s);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
child.stderr?.on("data", (d) => {
|
|
688
|
+
const s = d.toString().trim();
|
|
689
|
+
stderrTail.push(s);
|
|
690
|
+
if (stderrTail.length > 30)
|
|
691
|
+
stderrTail.shift();
|
|
692
|
+
log("migrate", `stderr: ${s}`);
|
|
693
|
+
});
|
|
694
|
+
child.on("error", (e) => {
|
|
695
|
+
deps.vertical.reportCrash("child-exit", { child: "migrations", spawnError: e.message });
|
|
696
|
+
reject(e);
|
|
697
|
+
});
|
|
698
|
+
child.on("exit", (code) => {
|
|
699
|
+
if (code === 0)
|
|
700
|
+
return resolve("runner");
|
|
701
|
+
deps.vertical.reportCrash("child-exit", { child: "migrations", code, stderr: stderrTail.join("\n") });
|
|
702
|
+
reject(new Error(`migrations exit ${code}`));
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/** État disque des données Meili (joint à la décision de readiness). */
|
|
707
|
+
function meiliDiskState() {
|
|
708
|
+
try {
|
|
709
|
+
const dataMs = path.join(deps.paths.meiliDataDir(), "data.ms");
|
|
710
|
+
if (!fs.existsSync(dataMs))
|
|
711
|
+
return { dataMsPresent: false };
|
|
712
|
+
let bytes = 0;
|
|
713
|
+
for (const f of fs.readdirSync(dataMs)) {
|
|
714
|
+
try {
|
|
715
|
+
bytes += fs.statSync(path.join(dataMs, f)).size;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
/* fichier volatil */
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return { dataMsPresent: true, dataMsBytes: bytes };
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return { dataMsPresent: false };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Indexes GED prêts ? Fingerprint + chaque index attendu peuplé si SQL > 0.
|
|
729
|
+
* (remplace l'ancien totalDocs > 0 qui skippait dès que le 1er index était plein)
|
|
730
|
+
*/
|
|
731
|
+
async function meiliIndexReady(m) {
|
|
732
|
+
const started = Date.now();
|
|
733
|
+
let decision;
|
|
734
|
+
try {
|
|
735
|
+
decision = await deps.hosts
|
|
736
|
+
.meiliCoherence()
|
|
737
|
+
.decideMeiliReady(m, deps.paths.dbPath());
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
741
|
+
// La vérification de cohérence est une optimisation de recherche : un
|
|
742
|
+
// script absent/corrompu ne doit jamais bloquer l'ouverture du CRM.
|
|
743
|
+
log("main", `meili.ready skip (cohérence indisponible): ${reason}`);
|
|
744
|
+
trackDecision("meili.ready", "skip-coherence-unavailable", {
|
|
745
|
+
reason: reason.slice(0, 300),
|
|
746
|
+
durationMs: Date.now() - started,
|
|
747
|
+
});
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
log("main", `meili.ready ready=${decision.ready} reason=${decision.reason} ` +
|
|
751
|
+
`sql=${JSON.stringify(decision.sql)} meili=${JSON.stringify(decision.meili)}`);
|
|
752
|
+
trackDecision("meili.ready", decision.ready ? "skip" : "full-reindex", {
|
|
753
|
+
reason: decision.reason,
|
|
754
|
+
durationMs: Date.now() - started,
|
|
755
|
+
ctx: {
|
|
756
|
+
sql: decision.sql,
|
|
757
|
+
meili: decision.meili,
|
|
758
|
+
sqliteSchema: decision.sqliteSchema,
|
|
759
|
+
fingerprintPresent: Boolean(decision.fingerprint),
|
|
760
|
+
fingerprintBuiltAt: decision.fingerprint?.builtAt,
|
|
761
|
+
interruptedPrevious: decision.interruptedPrevious,
|
|
762
|
+
...meiliDiskState(),
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
return decision;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Indexation Meili BLOQUANTE avec progression live (lignes {ENVPREFIX}PROGRESS émises
|
|
769
|
+
* par meili-indexer sur stdout : {done, total} → %, ETA rafraîchie chaque
|
|
770
|
+
* seconde). L'app n'affiche pas la recherche tant que l'index n'est pas prêt.
|
|
771
|
+
*/
|
|
772
|
+
function runIndexerWithProgress(m) {
|
|
773
|
+
return new Promise((resolve, reject) => {
|
|
774
|
+
const script = deps.paths.nodeScript("meili-indexer.js");
|
|
775
|
+
const bin = deps.paths.nodeBinary();
|
|
776
|
+
const env = envForNodeScriptSpawn(bin);
|
|
777
|
+
const nm = deps.paths.nodeModulesPathForScripts();
|
|
778
|
+
if (nm)
|
|
779
|
+
env.NODE_PATH = nm;
|
|
780
|
+
env.DB_PATH = deps.paths.dbPath();
|
|
781
|
+
env.MEILI_HOST = m.host;
|
|
782
|
+
env.MEILI_MASTER_KEY = m.masterKey;
|
|
783
|
+
env[`${deps.envPrefix}_APP_VERSION`] = app.getVersion();
|
|
784
|
+
log("indexer", `indexation initiale (${script})`);
|
|
785
|
+
const child = spawn(bin, [script], {
|
|
786
|
+
env,
|
|
787
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
788
|
+
windowsHide: true,
|
|
789
|
+
});
|
|
790
|
+
const rate = new (deps.hosts.catalog().RateEstimator)();
|
|
791
|
+
let done = 0;
|
|
792
|
+
let total = 0;
|
|
793
|
+
const stderrTail = [];
|
|
794
|
+
// ETA rafraîchie chaque seconde même entre deux lots.
|
|
795
|
+
const ticker = setInterval(() => {
|
|
796
|
+
if (total <= 0)
|
|
797
|
+
return;
|
|
798
|
+
const { eta } = rate.etaFor(done, total);
|
|
799
|
+
setSplashProgress({
|
|
800
|
+
phase: "index",
|
|
801
|
+
percent: (done / total) * 100,
|
|
802
|
+
doneDocs: done,
|
|
803
|
+
totalDocs: total,
|
|
804
|
+
etaSeconds: eta,
|
|
805
|
+
detail: `${done.toLocaleString("fr-FR")} / ${total.toLocaleString("fr-FR")} documents · reste ${deps.hosts.catalog().formatEta(eta)}`,
|
|
806
|
+
});
|
|
807
|
+
}, 1000);
|
|
808
|
+
child.stdout?.on("data", (d) => {
|
|
809
|
+
for (const line of d.toString().split("\n")) {
|
|
810
|
+
const trimmed = line.trim();
|
|
811
|
+
if (!trimmed)
|
|
812
|
+
continue;
|
|
813
|
+
if (trimmed.startsWith(progressPrefix)) {
|
|
814
|
+
try {
|
|
815
|
+
const p = JSON.parse(trimmed.slice(progressPrefix.length));
|
|
816
|
+
done = p.done;
|
|
817
|
+
total = p.total;
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
/* ligne malformée */
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else if (!consumeOpsLine("indexer", trimmed)) {
|
|
824
|
+
log("indexer", trimmed);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
child.stderr?.on("data", (d) => {
|
|
829
|
+
const s = d.toString().trim();
|
|
830
|
+
stderrTail.push(s);
|
|
831
|
+
if (stderrTail.length > 30)
|
|
832
|
+
stderrTail.shift();
|
|
833
|
+
log("indexer", `stderr: ${s}`);
|
|
834
|
+
});
|
|
835
|
+
child.on("error", (e) => {
|
|
836
|
+
clearInterval(ticker);
|
|
837
|
+
deps.vertical.reportCrash("child-exit", { child: "meili-indexer", spawnError: e.message });
|
|
838
|
+
reject(e);
|
|
839
|
+
});
|
|
840
|
+
child.on("exit", (code) => {
|
|
841
|
+
clearInterval(ticker);
|
|
842
|
+
log("indexer", `terminé (code ${code})`);
|
|
843
|
+
if (code === 0)
|
|
844
|
+
return resolve();
|
|
845
|
+
deps.vertical.reportCrash("child-exit", {
|
|
846
|
+
child: "meili-indexer",
|
|
847
|
+
code,
|
|
848
|
+
stderr: stderrTail.join("\n"),
|
|
849
|
+
});
|
|
850
|
+
reject(new Error(`indexation Meilisearch échouée (code ${code})`));
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
/* ────────────────────────────── Auto-login ─────────────────────────────── */
|
|
855
|
+
/** Pose le cookie de session dans la partition CRM (pas les partitions fournisseurs). */
|
|
856
|
+
async function autoLogin(baseUrl) {
|
|
857
|
+
const auth = deps.store().getLocalAuth();
|
|
858
|
+
if (!auth)
|
|
859
|
+
return;
|
|
860
|
+
const res = await fetch(`${baseUrl}/api/v1/auth/login`, {
|
|
861
|
+
method: "POST",
|
|
862
|
+
headers: { "Content-Type": "application/json" },
|
|
863
|
+
body: JSON.stringify({ email: auth.authUser, password: auth.authPassword }),
|
|
864
|
+
signal: AbortSignal.timeout(10000),
|
|
865
|
+
});
|
|
866
|
+
const setCookie = res.headers.get("set-cookie") || "";
|
|
867
|
+
const m = setCookie.match(new RegExp(`${deps.sessionCookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]+)`));
|
|
868
|
+
if (!m)
|
|
869
|
+
return; // login refusé : l'UI affichera /login
|
|
870
|
+
const ses = session.fromPartition(deps.sessionPartition);
|
|
871
|
+
await ses.cookies.set({
|
|
872
|
+
url: baseUrl,
|
|
873
|
+
name: deps.sessionCookieName,
|
|
874
|
+
value: m[1],
|
|
875
|
+
httpOnly: true,
|
|
876
|
+
sameSite: "lax",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
/* ─────────────────────── Liens externes → onglets ──────────────────────── */
|
|
880
|
+
/** Id de site opaque : entier historique ou UUID marque — jamais NaN. */
|
|
881
|
+
function normalizeSiteId(raw) {
|
|
882
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
883
|
+
const asNum = Number(raw);
|
|
884
|
+
return Number.isFinite(asNum) && String(asNum) === raw.trim() ? asNum : raw.trim();
|
|
885
|
+
}
|
|
886
|
+
const n = Number(raw);
|
|
887
|
+
return Number.isFinite(n) ? n : 0;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Routage des liens EXTERNES : onglet site intégré + event renderer.
|
|
891
|
+
* SoT = site externe (pas « fournisseur »). Dual-emit wire legacy TF.
|
|
892
|
+
*/
|
|
893
|
+
function emitExternalTabOpened(view, info) {
|
|
894
|
+
const payload = {
|
|
895
|
+
...info,
|
|
896
|
+
fournisseurId: info.siteId, // miroir déprécié
|
|
897
|
+
};
|
|
898
|
+
try {
|
|
899
|
+
if (!view.webContents.isDestroyed()) {
|
|
900
|
+
view.webContents.send("tabs:external-opened", payload);
|
|
901
|
+
view.webContents.send("tabs:supplier-opened", payload); // alias déprécié
|
|
902
|
+
// Alias historique (certains preload écoutent encore ce nom)
|
|
903
|
+
view.webContents.send("desktop:supplier-tab-opened", payload);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
catch (e) {
|
|
907
|
+
logError("tabs", e);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/** @deprecated → emitExternalTabOpened */
|
|
911
|
+
function emitSupplierTabOpened(view, info) {
|
|
912
|
+
emitExternalTabOpened(view, {
|
|
913
|
+
tabId: info.tabId,
|
|
914
|
+
siteId: info.fournisseurId,
|
|
915
|
+
url: info.url,
|
|
916
|
+
title: info.title,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/** Manager d’onglets du sender IPC (owner ou espace IA). */
|
|
920
|
+
function tabsManagerForEvent(e) {
|
|
921
|
+
const info = aiWorkspaces?.findByWebContentsId(e.sender.id);
|
|
922
|
+
if (info) {
|
|
923
|
+
const m = aiWorkspaces.getTabs(info.userId);
|
|
924
|
+
if (m)
|
|
925
|
+
return m;
|
|
926
|
+
}
|
|
927
|
+
if (!tabs)
|
|
928
|
+
throw new Error("tabs manager absent");
|
|
929
|
+
return tabs;
|
|
930
|
+
}
|
|
931
|
+
function tabsManagerForView(view) {
|
|
932
|
+
try {
|
|
933
|
+
const info = aiWorkspaces?.findByWebContentsId(view.webContents.id);
|
|
934
|
+
if (info)
|
|
935
|
+
return aiWorkspaces.getTabs(info.userId);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
/* ignore */
|
|
939
|
+
}
|
|
940
|
+
return tabs;
|
|
941
|
+
}
|
|
942
|
+
function routeExternalLinksToTabs(view, baseUrl) {
|
|
943
|
+
const openInTab = (url, fournisseurId) => {
|
|
944
|
+
const manager = tabsManagerForView(view);
|
|
945
|
+
if (!manager)
|
|
946
|
+
return;
|
|
947
|
+
void manager
|
|
948
|
+
.openTab(fournisseurId, url)
|
|
949
|
+
.then((tab) => {
|
|
950
|
+
const title = tab.view.webContents.isDestroyed()
|
|
951
|
+
? url
|
|
952
|
+
: tab.view.webContents.getTitle() || url;
|
|
953
|
+
const finalUrl = tab.view.webContents.isDestroyed()
|
|
954
|
+
? url
|
|
955
|
+
: tab.view.webContents.getURL() || url;
|
|
956
|
+
emitSupplierTabOpened(view, {
|
|
957
|
+
tabId: tab.tabId,
|
|
958
|
+
fournisseurId: tab.fournisseurId,
|
|
959
|
+
url: finalUrl,
|
|
960
|
+
title,
|
|
961
|
+
});
|
|
962
|
+
})
|
|
963
|
+
.catch((e) => {
|
|
964
|
+
logError("tabs", e);
|
|
965
|
+
deps.vertical.reportCrash("tab-error", {
|
|
966
|
+
step: "openInTab",
|
|
967
|
+
url: String(url).slice(0, 300),
|
|
968
|
+
fournisseurId,
|
|
969
|
+
message: e instanceof Error ? e.message : String(e),
|
|
970
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
};
|
|
974
|
+
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
975
|
+
try {
|
|
976
|
+
const u = new URL(url);
|
|
977
|
+
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
978
|
+
if (isBinaryDownloadUrl(url)) {
|
|
979
|
+
// Installeur / archive : navigateur système. Un onglet interne
|
|
980
|
+
// resterait blanc par-dessus l'UI le temps du téléchargement
|
|
981
|
+
// (bug cockpit → Invitations → « téléchargement »).
|
|
982
|
+
void shell.openExternal(url).catch((e) => logError("tabs", e));
|
|
983
|
+
}
|
|
984
|
+
else if (url.startsWith(baseUrl)) {
|
|
985
|
+
// Lien interne ouvert en _blank : navigation dans la vue CRM.
|
|
986
|
+
void view.webContents.loadURL(url);
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
// supplierFidQueryParam : id fournisseur (SiteLink).
|
|
990
|
+
const fidParam = supplierFidQueryParam;
|
|
991
|
+
const fid = Number(u.searchParams.get(fidParam) || "0") || 0;
|
|
992
|
+
u.searchParams.delete(fidParam);
|
|
993
|
+
openInTab(u.toString(), fid);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
/* URL invalide : ignorée */
|
|
999
|
+
}
|
|
1000
|
+
return { action: "deny" };
|
|
1001
|
+
});
|
|
1002
|
+
// Lien externe SANS target=_blank : ne jamais quitter l'UI CRM.
|
|
1003
|
+
view.webContents.on("will-navigate", (event, url) => {
|
|
1004
|
+
if (!url.startsWith(baseUrl) && !url.startsWith("data:")) {
|
|
1005
|
+
event.preventDefault();
|
|
1006
|
+
if (isBinaryDownloadUrl(url)) {
|
|
1007
|
+
void shell.openExternal(url).catch((e) => logError("tabs", e));
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
openInTab(url, 0);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* URL de binaire téléchargeable (installeurs desktop, archives) : à ouvrir
|
|
1017
|
+
* dans le navigateur de l'OS, jamais dans un onglet/vue interne.
|
|
1018
|
+
*/
|
|
1019
|
+
const BINARY_DOWNLOAD_RE = /\.(exe|msi|dmg|pkg|appimage|zip|7z)$/i;
|
|
1020
|
+
function isBinaryDownloadUrl(raw) {
|
|
1021
|
+
try {
|
|
1022
|
+
return BINARY_DOWNLOAD_RE.test(new URL(raw).pathname);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Garde-fou de l'app Serveur (kind=server) : la fenêtre principale n'affiche
|
|
1030
|
+
* QUE le cockpit autonome. Toute navigation même-origin vers l'UI CRM
|
|
1031
|
+
* (/dashboard, /admin, /produits…) est rabattue sur /server-cockpit —
|
|
1032
|
+
* y compris les navigations in-page du router Next (non annulables).
|
|
1033
|
+
*/
|
|
1034
|
+
function installServerCockpitGuard(view, baseUrl) {
|
|
1035
|
+
const cockpitUrl = `${baseUrl}${deps.bootBehavior.cockpitPath}`;
|
|
1036
|
+
const allowed = (url) => {
|
|
1037
|
+
if (!url.startsWith(baseUrl))
|
|
1038
|
+
return true; // externe / data: → handlers existants
|
|
1039
|
+
try {
|
|
1040
|
+
return deps.vertical.isAllowedServerCockpitPath(new URL(url).pathname);
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
view.webContents.on("will-navigate", (event, url) => {
|
|
1047
|
+
if (!allowed(url)) {
|
|
1048
|
+
event.preventDefault();
|
|
1049
|
+
log("main", `garde-fou cockpit : navigation bloquée → ${url}`);
|
|
1050
|
+
void view.webContents.loadURL(cockpitUrl).catch(() => { });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
view.webContents.on("did-navigate-in-page", (_e, url) => {
|
|
1054
|
+
if (!allowed(url)) {
|
|
1055
|
+
log("main", `garde-fou cockpit : navigation in-page rabattue → ${url}`);
|
|
1056
|
+
void view.webContents.loadURL(cockpitUrl).catch(() => { });
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
/* ─────────────────────────────── IPC ───────────────────────────────────── */
|
|
1061
|
+
/**
|
|
1062
|
+
* Refus attendu d'un canal host-only sur l'app Client : rejet propre côté
|
|
1063
|
+
* renderer, mais PAS de deps.vertical.reportCrash (sinon chaque clic sur un bouton non
|
|
1064
|
+
* gardé de l'UI distante spammerait le collecteur).
|
|
1065
|
+
*/
|
|
1066
|
+
class HostOnlyRefusedError extends Error {
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* ipcMain.handle blindé : toute exception est loggée + rapportée au
|
|
1070
|
+
* collecteur (kind "tab-error") puis re-levée en Error propre — le renderer
|
|
1071
|
+
* reçoit un rejet de promesse explicite, l'app ne tombe JAMAIS.
|
|
1072
|
+
*/
|
|
1073
|
+
function safeHandle(channel, fn) {
|
|
1074
|
+
ipcMain.handle(channel, async (event, ...args) => {
|
|
1075
|
+
try {
|
|
1076
|
+
return await fn(event, ...args);
|
|
1077
|
+
}
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
logError("ipc", e);
|
|
1080
|
+
if (!(e instanceof HostOnlyRefusedError)) {
|
|
1081
|
+
deps.vertical.reportCrash("tab-error", {
|
|
1082
|
+
step: `ipc:${channel}`,
|
|
1083
|
+
args: JSON.stringify(args).slice(0, 500),
|
|
1084
|
+
message: e instanceof Error ? e.message : String(e),
|
|
1085
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(e instanceof Error ? e.message : String(e));
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Variante hôte-uniquement : canaux qui pilotent la stack locale (spawn
|
|
1094
|
+
* Hermes/n8n, plugins, tunnel, reindex, setup hôte…). L'app Client
|
|
1095
|
+
* (kind=client, allowLocalStack=false) ne doit JAMAIS pouvoir les déclencher,
|
|
1096
|
+
* même si l'UI distante en expose un bouton — défense en profondeur au-delà
|
|
1097
|
+
* des checks `connectionMode` individuels.
|
|
1098
|
+
*/
|
|
1099
|
+
function hostOnlyHandle(channel, fn) {
|
|
1100
|
+
safeHandle(channel, (event, ...args) => {
|
|
1101
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
1102
|
+
throw new HostOnlyRefusedError(`Action réservée à l'app Serveur (${channel} refusé sur un client léger)`);
|
|
1103
|
+
}
|
|
1104
|
+
return fn(event, ...args);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function registerIpc(view) {
|
|
1108
|
+
const ownerManager = tabs;
|
|
1109
|
+
safeHandle("desktop:info", () => ({
|
|
1110
|
+
version: app.getVersion(),
|
|
1111
|
+
serverPort: server?.port ?? 0,
|
|
1112
|
+
license: deps.vertical.checkLicense().state,
|
|
1113
|
+
platform: process.platform,
|
|
1114
|
+
customWindowChrome: process.platform === "win32",
|
|
1115
|
+
connectionMode: activeConnectionProfile.mode,
|
|
1116
|
+
baseUrl: crmBaseUrl(),
|
|
1117
|
+
localBind: activeConnectionProfile.localBind ?? "127.0.0.1",
|
|
1118
|
+
/** Split 0.10.0 : "server" | "client" | "legacy". */
|
|
1119
|
+
appKind: deps.appKind,
|
|
1120
|
+
}));
|
|
1121
|
+
safeHandle("connection:get", () => {
|
|
1122
|
+
const p = deps.store().getConnectionProfile();
|
|
1123
|
+
return {
|
|
1124
|
+
mode: activeConnectionProfile.mode || p.mode,
|
|
1125
|
+
remoteUrl: activeConnectionProfile.remoteUrl ?? p.remoteUrl ?? null,
|
|
1126
|
+
localBind: activeConnectionProfile.localBind ?? p.localBind ?? "127.0.0.1",
|
|
1127
|
+
chosen: (activeConnectionProfile.chosen ?? p.chosen) !== false,
|
|
1128
|
+
activeBaseUrl: crmBaseUrl(),
|
|
1129
|
+
serverPort: server?.port ?? null,
|
|
1130
|
+
};
|
|
1131
|
+
});
|
|
1132
|
+
safeHandle("connection:test", async (_e, rawUrl) => {
|
|
1133
|
+
// Lien d'invitation collé tel quel (tempoflow://join/…) : accepté
|
|
1134
|
+
// partout où on saisit une URL de serveur.
|
|
1135
|
+
const raw = String(rawUrl || "").trim();
|
|
1136
|
+
const fromDeepLink = deps.vertical.parseJoinDeepLink(raw);
|
|
1137
|
+
return deps.vertical.testRemoteHealth(fromDeepLink || raw);
|
|
1138
|
+
});
|
|
1139
|
+
/** Écran de profils : oublier un serveur mémorisé (tuile ✕). */
|
|
1140
|
+
safeHandle("profiles:forget-server", (_e, rawId) => {
|
|
1141
|
+
const id = String(rawId || "").trim();
|
|
1142
|
+
if (!id)
|
|
1143
|
+
return { ok: false, error: "id requis" };
|
|
1144
|
+
const servers = deps.store().forgetRememberedServer(id);
|
|
1145
|
+
return { ok: true, servers };
|
|
1146
|
+
});
|
|
1147
|
+
safeHandle("connection:choose", async (_e, rawProfile) => {
|
|
1148
|
+
try {
|
|
1149
|
+
const raw = (rawProfile || {});
|
|
1150
|
+
if (deps.bootBehavior.requireRemoteProfile && raw.mode !== "remote") {
|
|
1151
|
+
return {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
error: `Cette app est le client ${productName} : installez « ${productNameServer} » pour héberger.`,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
const prev = deps.store().getConnectionProfile();
|
|
1157
|
+
// Boot picker n'envoie plus localBind : on conserve la valeur hôte déjà
|
|
1158
|
+
// enregistrée (Configuration avancée). Défaut produit = loopback.
|
|
1159
|
+
const localBind = raw.localBind === "0.0.0.0" || raw.localBind === "127.0.0.1"
|
|
1160
|
+
? raw.localBind
|
|
1161
|
+
: prev.localBind === "0.0.0.0"
|
|
1162
|
+
? "0.0.0.0"
|
|
1163
|
+
: "127.0.0.1";
|
|
1164
|
+
const ready = deps.vertical.assertProfileReady({
|
|
1165
|
+
mode: raw.mode === "remote" ? "remote" : "local",
|
|
1166
|
+
remoteUrl: raw.remoteUrl,
|
|
1167
|
+
localBind,
|
|
1168
|
+
chosen: true,
|
|
1169
|
+
});
|
|
1170
|
+
if (ready.mode === "remote") {
|
|
1171
|
+
const health = await deps.vertical.testRemoteHealth(ready.remoteUrl || "");
|
|
1172
|
+
if (!health.ok) {
|
|
1173
|
+
return { ok: false, error: health.error || "Serveur injoignable" };
|
|
1174
|
+
}
|
|
1175
|
+
ready.remoteUrl = health.baseUrl || ready.remoteUrl;
|
|
1176
|
+
}
|
|
1177
|
+
const saved = deps.store().setConnectionProfile(ready);
|
|
1178
|
+
if (saved.mode === "remote" && saved.remoteUrl) {
|
|
1179
|
+
// Tuile « serveur mémorisé » sur l'écran de profils au prochain boot.
|
|
1180
|
+
deps.store().rememberServer(saved.remoteUrl);
|
|
1181
|
+
}
|
|
1182
|
+
activeConnectionProfile = saved;
|
|
1183
|
+
connectionChoiceResolver?.(saved);
|
|
1184
|
+
connectionChoiceResolver = null;
|
|
1185
|
+
return { ok: true, profile: saved };
|
|
1186
|
+
}
|
|
1187
|
+
catch (e) {
|
|
1188
|
+
return {
|
|
1189
|
+
ok: false,
|
|
1190
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
/** Depuis Configuration : enregistre + relance l'app pour appliquer le profil. */
|
|
1195
|
+
safeHandle("connection:apply", async (_e, rawProfile) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const raw = (rawProfile || {});
|
|
1198
|
+
if (deps.bootBehavior.requireRemoteProfile && raw.mode !== "remote") {
|
|
1199
|
+
return {
|
|
1200
|
+
ok: false,
|
|
1201
|
+
error: `Cette app est le client ${productName} : installez « ${productNameServer} » pour héberger.`,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
if (deps.bootBehavior.forceLocalProfile && raw.mode === "remote") {
|
|
1205
|
+
return {
|
|
1206
|
+
ok: false,
|
|
1207
|
+
error: `Cette app est le serveur ${productName} : utilisez l'app Client pour rejoindre un autre serveur.`,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const ready = deps.vertical.assertProfileReady({
|
|
1211
|
+
mode: raw.mode === "remote" ? "remote" : "local",
|
|
1212
|
+
remoteUrl: raw.remoteUrl,
|
|
1213
|
+
localBind: raw.localBind === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1",
|
|
1214
|
+
chosen: true,
|
|
1215
|
+
});
|
|
1216
|
+
if (ready.mode === "remote") {
|
|
1217
|
+
const health = await deps.vertical.testRemoteHealth(ready.remoteUrl || "");
|
|
1218
|
+
if (!health.ok) {
|
|
1219
|
+
return { ok: false, error: health.error || "Serveur injoignable" };
|
|
1220
|
+
}
|
|
1221
|
+
ready.remoteUrl = health.baseUrl || ready.remoteUrl;
|
|
1222
|
+
}
|
|
1223
|
+
deps.store().setConnectionProfile(ready);
|
|
1224
|
+
log("main", `profil connexion appliqué (${ready.mode}) — relance app`);
|
|
1225
|
+
setTimeout(() => {
|
|
1226
|
+
app.relaunch();
|
|
1227
|
+
app.quit();
|
|
1228
|
+
}, 150);
|
|
1229
|
+
return { ok: true, relaunching: true };
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
return {
|
|
1233
|
+
ok: false,
|
|
1234
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
/**
|
|
1239
|
+
* Depuis logout desktop / setup : efface le choix mémorisé et relance →
|
|
1240
|
+
* picker natif Héberger / Rejoindre (pas de factory-reset, pas de /login web).
|
|
1241
|
+
*/
|
|
1242
|
+
safeHandle("connection:rechoose", async () => {
|
|
1243
|
+
try {
|
|
1244
|
+
deps.store().clearConnectionProfileChoice();
|
|
1245
|
+
try {
|
|
1246
|
+
assistantChrome?.setMode("hidden");
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
/* ignore */
|
|
1250
|
+
}
|
|
1251
|
+
log("main", "rechoose connexion — picker au prochain boot");
|
|
1252
|
+
setTimeout(() => {
|
|
1253
|
+
app.relaunch();
|
|
1254
|
+
app.quit();
|
|
1255
|
+
}, 150);
|
|
1256
|
+
return { ok: true, relaunching: true };
|
|
1257
|
+
}
|
|
1258
|
+
catch (e) {
|
|
1259
|
+
return {
|
|
1260
|
+
ok: false,
|
|
1261
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
/* ── Chrome fenêtre (frameless Windows) ── */
|
|
1266
|
+
safeHandle("window:minimize", () => {
|
|
1267
|
+
win?.minimize();
|
|
1268
|
+
});
|
|
1269
|
+
safeHandle("window:maximize-toggle", () => {
|
|
1270
|
+
if (!win)
|
|
1271
|
+
return { isMaximized: false };
|
|
1272
|
+
if (win.isMaximized())
|
|
1273
|
+
win.unmaximize();
|
|
1274
|
+
else
|
|
1275
|
+
win.maximize();
|
|
1276
|
+
return { isMaximized: win.isMaximized() };
|
|
1277
|
+
});
|
|
1278
|
+
safeHandle("window:close", () => {
|
|
1279
|
+
win?.close();
|
|
1280
|
+
});
|
|
1281
|
+
safeHandle("window:isMaximized", () => win?.isMaximized() ?? false);
|
|
1282
|
+
/* ── Fonctionnement en arrière-plan (tray + démarrage auto) ── */
|
|
1283
|
+
safeHandle("background:get", () => ({
|
|
1284
|
+
...deps.store().getBackgroundSettings(),
|
|
1285
|
+
trayActive: trayController?.active ?? false,
|
|
1286
|
+
platform: process.platform,
|
|
1287
|
+
}));
|
|
1288
|
+
safeHandle("background:set", (_e, rawPatch) => {
|
|
1289
|
+
const raw = (rawPatch || {});
|
|
1290
|
+
const patch = {};
|
|
1291
|
+
if (typeof raw.closeToTray === "boolean")
|
|
1292
|
+
patch.closeToTray = raw.closeToTray;
|
|
1293
|
+
if (typeof raw.launchAtStartup === "boolean") {
|
|
1294
|
+
patch.launchAtStartup = raw.launchAtStartup;
|
|
1295
|
+
}
|
|
1296
|
+
const settings = deps.store().setBackgroundSettings(patch);
|
|
1297
|
+
if (typeof patch.launchAtStartup === "boolean") {
|
|
1298
|
+
if (app.isPackaged) {
|
|
1299
|
+
applyLaunchAtStartup(settings.launchAtStartup);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
log("tray", "dev : setLoginItemSettings ignoré (app non packagée)");
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
ok: true,
|
|
1307
|
+
settings,
|
|
1308
|
+
trayActive: trayController?.active ?? false,
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
safeHandle("tabs:open", async (e, a, b) => {
|
|
1312
|
+
const manager = tabsManagerForEvent(e);
|
|
1313
|
+
// Compat : (siteId, url) OU { siteId|fournisseurId, url }
|
|
1314
|
+
// siteId opaque : UUID marque (string) ou entier historique.
|
|
1315
|
+
let siteId;
|
|
1316
|
+
let openUrl;
|
|
1317
|
+
if (a && typeof a === "object") {
|
|
1318
|
+
const o = a;
|
|
1319
|
+
siteId = normalizeSiteId(o.siteId ?? o.fournisseurId);
|
|
1320
|
+
openUrl = String(o.url ?? "");
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
siteId = normalizeSiteId(a);
|
|
1324
|
+
openUrl = String(b ?? "");
|
|
1325
|
+
}
|
|
1326
|
+
const tab = await manager.openTab(siteId, openUrl);
|
|
1327
|
+
let url = "";
|
|
1328
|
+
try {
|
|
1329
|
+
if (!tab.view.webContents.isDestroyed())
|
|
1330
|
+
url = tab.view.webContents.getURL();
|
|
1331
|
+
}
|
|
1332
|
+
catch {
|
|
1333
|
+
/* ignore */
|
|
1334
|
+
}
|
|
1335
|
+
try {
|
|
1336
|
+
const host = url ? new URL(url).host : "";
|
|
1337
|
+
recordFleetAction({
|
|
1338
|
+
type: "embed.navigate",
|
|
1339
|
+
name: "embed.navigate",
|
|
1340
|
+
category: "embed",
|
|
1341
|
+
label: `Onglet site externe #${siteId}${host ? ` · ${host}` : ""}`,
|
|
1342
|
+
path: host ? `https://${host}/` : undefined,
|
|
1343
|
+
surface: "external_site_tab",
|
|
1344
|
+
meta: { siteId, fournisseurId: siteId, host },
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
catch {
|
|
1348
|
+
/* ignore */
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
tabId: tab.tabId,
|
|
1352
|
+
siteId: tab.siteId ?? siteId,
|
|
1353
|
+
fournisseurId: tab.siteId ?? tab.fournisseurId ?? siteId,
|
|
1354
|
+
loadState: tab.loadState,
|
|
1355
|
+
url: url || undefined,
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
safeHandle("tabs:close", (e, tabId) => tabsManagerForEvent(e).closeTab(String(tabId)));
|
|
1359
|
+
safeHandle("tabs:activate", (e, rawArgs) => {
|
|
1360
|
+
const manager = tabsManagerForEvent(e);
|
|
1361
|
+
const args = rawArgs;
|
|
1362
|
+
const rect = args.rect
|
|
1363
|
+
? args.rect
|
|
1364
|
+
: typeof args.topOffset === "number"
|
|
1365
|
+
? { x: 0, y: args.topOffset, width: 10_000, height: 10_000 }
|
|
1366
|
+
: undefined;
|
|
1367
|
+
return manager.activate(String(args.tabId), rect);
|
|
1368
|
+
});
|
|
1369
|
+
safeHandle("tabs:activate-site", (e, a, b, c) => {
|
|
1370
|
+
const manager = tabsManagerForEvent(e);
|
|
1371
|
+
// Compat : (siteId, url, rect?) OU { siteId, url, rect? }
|
|
1372
|
+
let siteId;
|
|
1373
|
+
let url;
|
|
1374
|
+
let rect;
|
|
1375
|
+
if (a && typeof a === "object" && !Array.isArray(a) && ("siteId" in a || "url" in a)) {
|
|
1376
|
+
const o = a;
|
|
1377
|
+
siteId = normalizeSiteId(o.siteId);
|
|
1378
|
+
url = String(o.url || "");
|
|
1379
|
+
rect = o.rect;
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
siteId = normalizeSiteId(a);
|
|
1383
|
+
url = String(b ?? "");
|
|
1384
|
+
rect = c;
|
|
1385
|
+
}
|
|
1386
|
+
return manager.activateSite(siteId, url, rect);
|
|
1387
|
+
});
|
|
1388
|
+
safeHandle("tabs:set-content-rect", (e, rawRect) => {
|
|
1389
|
+
const rect = rawRect;
|
|
1390
|
+
tabsManagerForEvent(e).setContentRect(rect);
|
|
1391
|
+
});
|
|
1392
|
+
safeHandle("tabs:show-crm", (e) => tabsManagerForEvent(e).showCrm());
|
|
1393
|
+
safeHandle("tabs:list", (e) => tabsManagerForEvent(e).list());
|
|
1394
|
+
/* ── Espaces workspace collaborateurs IA ── */
|
|
1395
|
+
safeHandle("ai-workspace:identity", (e) => {
|
|
1396
|
+
const wcId = e.sender.id;
|
|
1397
|
+
const info = aiWorkspaces?.findByWebContentsId(wcId) || null;
|
|
1398
|
+
if (!info)
|
|
1399
|
+
return { userId: null, label: "", active: false };
|
|
1400
|
+
return {
|
|
1401
|
+
userId: info.userId,
|
|
1402
|
+
label: info.label,
|
|
1403
|
+
active: info.active,
|
|
1404
|
+
};
|
|
1405
|
+
});
|
|
1406
|
+
safeHandle("ai-workspace:list", () => aiWorkspaces?.list() || []);
|
|
1407
|
+
safeHandle("ai-workspace:ensure", async (_e, raw) => {
|
|
1408
|
+
if (!aiWorkspaces)
|
|
1409
|
+
return { ok: false, error: "manager absent" };
|
|
1410
|
+
const args = raw;
|
|
1411
|
+
const baseUrl = (typeof args.baseUrl === "string" && args.baseUrl) ||
|
|
1412
|
+
crmBaseUrl() ||
|
|
1413
|
+
server?.baseUrl ||
|
|
1414
|
+
"";
|
|
1415
|
+
try {
|
|
1416
|
+
const info = await aiWorkspaces.ensure({
|
|
1417
|
+
userId: String(args.userId || ""),
|
|
1418
|
+
token: String(args.token || ""),
|
|
1419
|
+
baseUrl,
|
|
1420
|
+
label: args.label,
|
|
1421
|
+
});
|
|
1422
|
+
if (args.show)
|
|
1423
|
+
aiWorkspaces.show(info.userId);
|
|
1424
|
+
return { ok: true, workspace: info };
|
|
1425
|
+
}
|
|
1426
|
+
catch (err) {
|
|
1427
|
+
return {
|
|
1428
|
+
ok: false,
|
|
1429
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
safeHandle("ai-workspace:show", (_e, userId) => {
|
|
1434
|
+
if (!aiWorkspaces)
|
|
1435
|
+
return { ok: false, error: "manager absent" };
|
|
1436
|
+
try {
|
|
1437
|
+
const info = aiWorkspaces.show(String(userId || ""));
|
|
1438
|
+
return { ok: true, workspace: info };
|
|
1439
|
+
}
|
|
1440
|
+
catch (err) {
|
|
1441
|
+
return {
|
|
1442
|
+
ok: false,
|
|
1443
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
safeHandle("ai-workspace:show-owner", () => {
|
|
1448
|
+
aiWorkspaces?.showOwner();
|
|
1449
|
+
return { ok: true };
|
|
1450
|
+
});
|
|
1451
|
+
// P3 cockpit : « Ouvrir l'app admin » → fenêtre /dashboard séparée
|
|
1452
|
+
// (même partition session que la vue CRM principale — cookies owner).
|
|
1453
|
+
safeHandle("admin-window:open", async () => {
|
|
1454
|
+
// L'app Serveur ne doit JAMAIS afficher l'UI CRM, même en fenêtre à part
|
|
1455
|
+
// (audit split : trou admin-window sans garde cockpit). Legacy /cockpit
|
|
1456
|
+
// conserve ce chemin.
|
|
1457
|
+
if (deps.appKind === "server") {
|
|
1458
|
+
return {
|
|
1459
|
+
ok: false,
|
|
1460
|
+
error: "L'UI d'administration CRM s'ouvre depuis l'app Client (l'app Serveur n'affiche que le cockpit).",
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
const baseUrl = crmBaseUrl() || server?.baseUrl || "";
|
|
1464
|
+
if (!baseUrl)
|
|
1465
|
+
return { ok: false, error: "serveur non démarré" };
|
|
1466
|
+
const adminPreload = deps.paths.preloadPath("preload.js");
|
|
1467
|
+
return openAdminWindow({
|
|
1468
|
+
baseUrl,
|
|
1469
|
+
partition: deps.sessionPartition,
|
|
1470
|
+
productName: deps.manifest.client.productName,
|
|
1471
|
+
preloadPath: adminPreload,
|
|
1472
|
+
instrument: (v) => deps.vertical.instrumentWebContents(v.webContents, "admin-window"),
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
ipcMain.on("ai-workspace:action-result", (_e, raw) => {
|
|
1476
|
+
const payload = raw;
|
|
1477
|
+
if (!payload?.actionId || !aiWorkspaces)
|
|
1478
|
+
return;
|
|
1479
|
+
aiWorkspaces.resolvePending(String(payload.actionId), payload.result && typeof payload.result === "object"
|
|
1480
|
+
? payload.result
|
|
1481
|
+
: { ok: false, error: "result manquant" });
|
|
1482
|
+
});
|
|
1483
|
+
safeHandle("assistant:set-chrome", (_e, rawMode) => {
|
|
1484
|
+
const mode = String(rawMode) === "fab" ? "fab" : "hidden";
|
|
1485
|
+
assistantChrome?.setMode(mode);
|
|
1486
|
+
});
|
|
1487
|
+
if (typeof ownerManager?.setOnChanged === "function") {
|
|
1488
|
+
ownerManager.setOnChanged(() => {
|
|
1489
|
+
try {
|
|
1490
|
+
if (!view.webContents.isDestroyed()) {
|
|
1491
|
+
view.webContents.send("tabs:changed", typeof ownerManager.list === "function" ? ownerManager.list() : []);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
catch (e) {
|
|
1495
|
+
logError("tabs", e);
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (typeof ownerManager?.setOnLoadState === "function") {
|
|
1500
|
+
ownerManager.setOnLoadState((ev) => {
|
|
1501
|
+
try {
|
|
1502
|
+
if (!view.webContents.isDestroyed()) {
|
|
1503
|
+
view.webContents.send("tabs:load-state", ev);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
catch (e) {
|
|
1507
|
+
logError("tabs", e);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (typeof ownerManager?.setOnAfterBounds === "function") {
|
|
1512
|
+
ownerManager.setOnAfterBounds(() => {
|
|
1513
|
+
assistantChrome?.ensureTop();
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
// Erreurs JS de la page (window.onerror / unhandledrejection), forwardées
|
|
1517
|
+
// par le preload : log persistant + rapport débouncé au collecteur.
|
|
1518
|
+
ipcMain.on("renderer-error", (_e, rawPayload) => {
|
|
1519
|
+
try {
|
|
1520
|
+
const p = (rawPayload ?? {});
|
|
1521
|
+
const msg = String(p.message ?? "").slice(0, 1000);
|
|
1522
|
+
log("web", `[crm] ${p.kind}: ${msg} (${p.file ?? "?"}:${p.line ?? "?"})`);
|
|
1523
|
+
deps.vertical.reportCrashDebounced("renderer-error", `js:${msg.slice(0, 120)}`, {
|
|
1524
|
+
view: "crm",
|
|
1525
|
+
source: p.kind || "window-error",
|
|
1526
|
+
message: msg,
|
|
1527
|
+
stack: String(p.stack ?? "").slice(0, 4000),
|
|
1528
|
+
file: p.file,
|
|
1529
|
+
line: p.line,
|
|
1530
|
+
url: p.url,
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
catch (e) {
|
|
1534
|
+
logError("web", e);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
safeHandle("hermes:status", () => {
|
|
1538
|
+
if (!deps.bootBehavior.allowLocalStack)
|
|
1539
|
+
return clientHermesStatusPayload();
|
|
1540
|
+
return deps.hosts.hermes().getHermesStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
1541
|
+
});
|
|
1542
|
+
safeHandle("hermes:logs", () => deps.bootBehavior.allowLocalStack ? deps.hosts.hermes().getHermesLogs() : []);
|
|
1543
|
+
hostOnlyHandle("hermes:retry", async () => {
|
|
1544
|
+
if (activeConnectionProfile.mode !== "local") {
|
|
1545
|
+
return deps.hosts.hermes().getHermesStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
1546
|
+
}
|
|
1547
|
+
const current = deps.hosts.hermes().getRunningHermes();
|
|
1548
|
+
if (current?.webuiUrl) {
|
|
1549
|
+
hermes = current;
|
|
1550
|
+
return deps.hosts.hermes().getHermesStatusPayload("local");
|
|
1551
|
+
}
|
|
1552
|
+
if (current)
|
|
1553
|
+
await deps.hosts.hermes().stopHermesAndWait();
|
|
1554
|
+
const started = await deps.hosts.hermes().startHermes({
|
|
1555
|
+
connectionMode: "local",
|
|
1556
|
+
hermesConfig: deps.store().getHermesEmbedConfig(),
|
|
1557
|
+
autoBootstrap: true,
|
|
1558
|
+
crmPort: server?.port ?? null,
|
|
1559
|
+
onLog: (line) => log("hermes", line),
|
|
1560
|
+
});
|
|
1561
|
+
hermes = started;
|
|
1562
|
+
await maybeRestartNextAfterHermesSpawn(Boolean(started));
|
|
1563
|
+
const status = deps.hosts.hermes().getHermesStatusPayload("local");
|
|
1564
|
+
if (!started?.webuiUrl) {
|
|
1565
|
+
deps.vertical.reportCrashDebounced("child-exit", `hermes-retry:${status.detail}`, {
|
|
1566
|
+
service: "hermes",
|
|
1567
|
+
action: "ui-retry",
|
|
1568
|
+
status: status.status,
|
|
1569
|
+
webuiStatus: status.webuiStatus,
|
|
1570
|
+
detail: status.detail,
|
|
1571
|
+
bootstrapError: status.bootstrapError,
|
|
1572
|
+
logs: deps.hosts.hermes().getHermesLogs().slice(-80),
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
return status;
|
|
1576
|
+
});
|
|
1577
|
+
safeHandle("hermes:get-config", () => deps.store().getHermesEmbedConfig());
|
|
1578
|
+
hostOnlyHandle("hermes:set-config", async (_e, raw) => {
|
|
1579
|
+
const next = deps.store().setHermesEmbedConfig(deps.vertical.sanitizeHermesEmbedConfig(raw && typeof raw === "object"
|
|
1580
|
+
? raw
|
|
1581
|
+
: {}));
|
|
1582
|
+
// Changement de mode : demander un relaunch pour (re)spawner proprement.
|
|
1583
|
+
return { ok: true, config: next, relaunchRequired: true };
|
|
1584
|
+
});
|
|
1585
|
+
hostOnlyHandle("hermes:ensure-runtime", async () => {
|
|
1586
|
+
if (activeConnectionProfile.mode !== "local") {
|
|
1587
|
+
return {
|
|
1588
|
+
ok: false,
|
|
1589
|
+
detail: "Client distant — pas de runtime Hermes local.",
|
|
1590
|
+
binaryPath: null,
|
|
1591
|
+
webuiDir: null,
|
|
1592
|
+
relaunchRequired: false,
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const r = await deps.hosts.hermes().ensureHermesRuntimeFromUi({
|
|
1596
|
+
onLog: (line) => log("hermes", line),
|
|
1597
|
+
});
|
|
1598
|
+
return {
|
|
1599
|
+
...r,
|
|
1600
|
+
relaunchRequired: r.ok,
|
|
1601
|
+
};
|
|
1602
|
+
});
|
|
1603
|
+
safeHandle("n8n:status", () => {
|
|
1604
|
+
if (!deps.bootBehavior.allowLocalStack)
|
|
1605
|
+
return clientN8nStatusPayload();
|
|
1606
|
+
return deps.hosts.n8n().getN8nStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
1607
|
+
});
|
|
1608
|
+
safeHandle("n8n:logs", () => deps.bootBehavior.allowLocalStack ? deps.hosts.n8n().getN8nLogs() : []);
|
|
1609
|
+
safeHandle("n8n:get-config", () => deps.store().getN8nEmbedConfig());
|
|
1610
|
+
hostOnlyHandle("n8n:set-config", async (_e, raw) => {
|
|
1611
|
+
const next = deps.store().setN8nEmbedConfig(deps.vertical.sanitizeN8nEmbedConfig(raw && typeof raw === "object"
|
|
1612
|
+
? raw
|
|
1613
|
+
: {}));
|
|
1614
|
+
return { ok: true, config: next, relaunchRequired: true };
|
|
1615
|
+
});
|
|
1616
|
+
hostOnlyHandle("n8n:ensure-runtime", async () => {
|
|
1617
|
+
if (activeConnectionProfile.mode !== "local") {
|
|
1618
|
+
return {
|
|
1619
|
+
ok: false,
|
|
1620
|
+
detail: "Client distant — pas de runtime n8n local.",
|
|
1621
|
+
entryPath: null,
|
|
1622
|
+
runtimeDir: null,
|
|
1623
|
+
uiUrl: null,
|
|
1624
|
+
relaunchRequired: false,
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const r = await deps.hosts.n8n().ensureN8nRuntimeFromUi({
|
|
1628
|
+
onLog: (line) => log("n8n", line),
|
|
1629
|
+
});
|
|
1630
|
+
let uiUrl = n8n?.uiUrl ?? null;
|
|
1631
|
+
if (r.ok) {
|
|
1632
|
+
try {
|
|
1633
|
+
const started = await deps.hosts.n8n().startN8n({
|
|
1634
|
+
connectionMode: "local",
|
|
1635
|
+
n8nConfig: deps.store().getN8nEmbedConfig(),
|
|
1636
|
+
autoBootstrap: false,
|
|
1637
|
+
onLog: (line) => log("n8n", line),
|
|
1638
|
+
});
|
|
1639
|
+
if (started) {
|
|
1640
|
+
n8n = started;
|
|
1641
|
+
uiUrl = started.uiUrl;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
catch (e) {
|
|
1645
|
+
logError("n8n", e);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
...r,
|
|
1650
|
+
uiUrl,
|
|
1651
|
+
relaunchRequired: false,
|
|
1652
|
+
};
|
|
1653
|
+
});
|
|
1654
|
+
hostOnlyHandle("n8n:prepare-session", async () => deps.hosts.n8n().prepareN8nUiSession());
|
|
1655
|
+
safeHandle("embed-env:get", (_e, rawService) => {
|
|
1656
|
+
const service = String(rawService || "");
|
|
1657
|
+
if (!deps.vertical.isEmbedEnvService(service)) {
|
|
1658
|
+
throw new Error(`service env inconnu: ${service}`);
|
|
1659
|
+
}
|
|
1660
|
+
const lockedValues = {};
|
|
1661
|
+
if (service === "n8n") {
|
|
1662
|
+
const st = deps.hosts.n8n().getN8nStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
1663
|
+
lockedValues.WEBHOOK_URL = st.publicWebhookUrl;
|
|
1664
|
+
lockedValues.N8N_EDITOR_BASE_URL = st.publicWebhookUrl;
|
|
1665
|
+
lockedValues.N8N_LISTEN_ADDRESS = st.listenHost;
|
|
1666
|
+
lockedValues.N8N_PORT = String(st.listenPort);
|
|
1667
|
+
lockedValues.N8N_USER_FOLDER = st.homeDir;
|
|
1668
|
+
lockedValues.N8N_ENCRYPTION_KEY = `•••• (gérée par ${productName})`;
|
|
1669
|
+
lockedValues.N8N_MCP_ACCESS_ENABLED = "true";
|
|
1670
|
+
lockedValues.N8N_MCP_MANAGED_BY_ENV = "true";
|
|
1671
|
+
}
|
|
1672
|
+
else if (service === "hermes") {
|
|
1673
|
+
const st = deps.hosts.hermes().getHermesStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
1674
|
+
lockedValues.HERMES_HOME = st.homeDir;
|
|
1675
|
+
lockedValues.API_SERVER_HOST = "127.0.0.1";
|
|
1676
|
+
lockedValues.API_SERVER_PORT = st.apiUrl
|
|
1677
|
+
? String(new URL(st.apiUrl).port || "")
|
|
1678
|
+
: "";
|
|
1679
|
+
lockedValues.API_SERVER_KEY = `•••• (gérée par ${productName})`;
|
|
1680
|
+
lockedValues.TERMINAL_CWD = st.homeDir
|
|
1681
|
+
? `${st.homeDir.replace(/[/\\]$/, "")}/workspace`
|
|
1682
|
+
: "";
|
|
1683
|
+
}
|
|
1684
|
+
return deps.vertical.buildEmbedEnvPanel({
|
|
1685
|
+
service,
|
|
1686
|
+
userOverlay: deps.store().getEmbedUserEnv(service),
|
|
1687
|
+
lockedValues,
|
|
1688
|
+
});
|
|
1689
|
+
});
|
|
1690
|
+
safeHandle("plugins:status", async () => {
|
|
1691
|
+
// Client léger : les plugins vivent sur l'app Serveur — payload vide
|
|
1692
|
+
// propre plutôt que des sondes git/vendor sur des chemins absents.
|
|
1693
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
1694
|
+
return { root: "", plugins: [], running: [], logs: [] };
|
|
1695
|
+
}
|
|
1696
|
+
return deps.hosts.plugins().pluginsStatusPayloadWithGit();
|
|
1697
|
+
});
|
|
1698
|
+
hostOnlyHandle("plugins:set-enabled", async (_e, rawId, rawEnabled) => {
|
|
1699
|
+
const id = String(rawId || "").trim();
|
|
1700
|
+
const enabled = Boolean(rawEnabled);
|
|
1701
|
+
const p = deps.hosts.plugins().enablePlugin(id, enabled);
|
|
1702
|
+
if (!p)
|
|
1703
|
+
throw new Error(`plugin inconnu: ${id}`);
|
|
1704
|
+
recordFleetAction({
|
|
1705
|
+
type: "plugin.toggle",
|
|
1706
|
+
name: "plugin.toggle",
|
|
1707
|
+
category: "system",
|
|
1708
|
+
label: `${enabled ? "Active" : "Désactive"} plugin ${id}`,
|
|
1709
|
+
surface: "system",
|
|
1710
|
+
meta: { pluginId: id, enabled },
|
|
1711
|
+
});
|
|
1712
|
+
if (enabled) {
|
|
1713
|
+
await deps.hosts.plugins().startEnabledPlugins({ onLog: (line) => scoped("plugins")(line) });
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
ok: true,
|
|
1717
|
+
plugin: p,
|
|
1718
|
+
status: await deps.hosts.plugins().pluginsStatusPayloadWithGit(),
|
|
1719
|
+
};
|
|
1720
|
+
});
|
|
1721
|
+
hostOnlyHandle("plugins:scaffold", async (_e, raw) => {
|
|
1722
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1723
|
+
const id = String(o.id || "");
|
|
1724
|
+
const grant = deps.hosts.pluginControl().validatePluginExecutionGrant({
|
|
1725
|
+
token: String(o.executionGrant || ""),
|
|
1726
|
+
pluginId: id,
|
|
1727
|
+
action: "create",
|
|
1728
|
+
});
|
|
1729
|
+
if (!grant.ok)
|
|
1730
|
+
throw new Error(grant.error);
|
|
1731
|
+
const r = await deps.hosts.plugins().createPluginScaffoldWithGit({
|
|
1732
|
+
id,
|
|
1733
|
+
name: o.name != null ? String(o.name) : undefined,
|
|
1734
|
+
description: o.description != null ? String(o.description) : undefined,
|
|
1735
|
+
});
|
|
1736
|
+
if (!r.ok)
|
|
1737
|
+
throw new Error(r.error);
|
|
1738
|
+
await deps.hosts.plugins().startEnabledPlugins({ onLog: (line) => scoped("plugins")(line) });
|
|
1739
|
+
return {
|
|
1740
|
+
ok: true,
|
|
1741
|
+
plugin: r.plugin,
|
|
1742
|
+
git: r.git,
|
|
1743
|
+
status: await deps.hosts.plugins().pluginsStatusPayloadWithGit(),
|
|
1744
|
+
};
|
|
1745
|
+
});
|
|
1746
|
+
hostOnlyHandle("plugins:execution-grant", async (_e, raw) => {
|
|
1747
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1748
|
+
return deps.hosts.pluginControl().createPluginExecutionGrant({
|
|
1749
|
+
productId: String(o.productId || ""),
|
|
1750
|
+
prdRevisionId: String(o.prdRevisionId || ""),
|
|
1751
|
+
pluginId: String(o.pluginId || ""),
|
|
1752
|
+
ttlSeconds: Number(o.ttlSeconds || 600),
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
hostOnlyHandle("plugins:run-tests", async (_e, rawId) => deps.hosts.pluginTests().runPluginTests(String(rawId || "").trim()));
|
|
1756
|
+
hostOnlyHandle("plugins:data-migrate", async (_e, rawId) => deps.hosts.pluginControl().migratePluginData(String(rawId || "").trim()));
|
|
1757
|
+
// Redémarrage métier « Mettre à jour » : migrations DB auto puis relance.
|
|
1758
|
+
hostOnlyHandle("plugins:restart", async (_e, rawId) => {
|
|
1759
|
+
const id = String(rawId || "").trim();
|
|
1760
|
+
// Migrations data AVANT la relance, en sous-process Node vanilla
|
|
1761
|
+
// (better-sqlite3 est interdit dans le main Electron — cf. plugin-data.ts).
|
|
1762
|
+
// Best-effort : un échec de migration n'empêche pas le restart.
|
|
1763
|
+
if (fs.existsSync(path.join(deps.hosts.pluginRuntime().pluginsRootDir(), id, "migrations"))) {
|
|
1764
|
+
try {
|
|
1765
|
+
await deps.hosts.pluginControl().migratePluginData(id);
|
|
1766
|
+
}
|
|
1767
|
+
catch (e) {
|
|
1768
|
+
scoped("plugins")(`migrations data échouées pour ${id} — ${e instanceof Error ? e.message : String(e)}`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const r = await deps.hosts.plugins().restartPlugin(id);
|
|
1772
|
+
if (!r.ok)
|
|
1773
|
+
throw new Error(r.error);
|
|
1774
|
+
return {
|
|
1775
|
+
ok: true,
|
|
1776
|
+
running: r.running,
|
|
1777
|
+
status: await deps.hosts.plugins().pluginsStatusPayloadWithGit(),
|
|
1778
|
+
};
|
|
1779
|
+
});
|
|
1780
|
+
hostOnlyHandle("plugins:archive-runtime", async (_e, rawId) => deps.hosts.pluginControl().archivePluginRuntime(String(rawId || "").trim()));
|
|
1781
|
+
hostOnlyHandle("plugins:delete", async (_e, rawId) => {
|
|
1782
|
+
const id = String(rawId || "").trim();
|
|
1783
|
+
const r = deps.hosts.plugins().deletePlugin(id);
|
|
1784
|
+
if (!r.ok)
|
|
1785
|
+
throw new Error(r.error);
|
|
1786
|
+
recordFleetAction({
|
|
1787
|
+
type: "plugin.delete",
|
|
1788
|
+
name: "plugin.delete",
|
|
1789
|
+
category: "system",
|
|
1790
|
+
label: `Supprime plugin ${id}`,
|
|
1791
|
+
surface: "system",
|
|
1792
|
+
meta: { pluginId: id },
|
|
1793
|
+
});
|
|
1794
|
+
return {
|
|
1795
|
+
ok: true,
|
|
1796
|
+
deleted: r.deleted,
|
|
1797
|
+
status: await deps.hosts.plugins().pluginsStatusPayloadWithGit(),
|
|
1798
|
+
};
|
|
1799
|
+
});
|
|
1800
|
+
safeHandle("plugins:versions", async (_e, rawId) => {
|
|
1801
|
+
const id = String(rawId || "").trim();
|
|
1802
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
1803
|
+
return {
|
|
1804
|
+
ok: false,
|
|
1805
|
+
pluginId: id,
|
|
1806
|
+
available: false,
|
|
1807
|
+
commits: [],
|
|
1808
|
+
head: null,
|
|
1809
|
+
error: "Plugins gérés par l'app Serveur",
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
return deps.hosts.plugins().getPluginVersions(id);
|
|
1813
|
+
});
|
|
1814
|
+
hostOnlyHandle("plugins:restore-version", async (_e, rawId, rawRef) => {
|
|
1815
|
+
const id = String(rawId || "").trim();
|
|
1816
|
+
const ref = String(rawRef || "").trim();
|
|
1817
|
+
const r = await deps.hosts.plugins().restorePluginToVersion(id, ref);
|
|
1818
|
+
if (!r.ok)
|
|
1819
|
+
throw new Error(r.error);
|
|
1820
|
+
return {
|
|
1821
|
+
ok: true,
|
|
1822
|
+
sha: r.sha,
|
|
1823
|
+
detail: r.detail,
|
|
1824
|
+
running: r.running,
|
|
1825
|
+
status: await deps.hosts.plugins().pluginsStatusPayloadWithGit(),
|
|
1826
|
+
};
|
|
1827
|
+
});
|
|
1828
|
+
// Boîte noire : événements structurés depuis le renderer (UI CRM).
|
|
1829
|
+
safeHandle("ops:track", (_e, raw) => ({
|
|
1830
|
+
ok: trackExternal(raw, "renderer"),
|
|
1831
|
+
}));
|
|
1832
|
+
hostOnlyHandle("fleet:get-telemetry", () => deps.store().getFleetTelemetry());
|
|
1833
|
+
hostOnlyHandle("fleet:action", (_e, raw) => {
|
|
1834
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1835
|
+
const meta = o.meta && typeof o.meta === "object"
|
|
1836
|
+
? o.meta
|
|
1837
|
+
: {};
|
|
1838
|
+
if (meta.setContextOnly === true || o.type === "session.context") {
|
|
1839
|
+
setFleetSessionContext({
|
|
1840
|
+
userId: o.userId != null ? String(o.userId) : null,
|
|
1841
|
+
username: o.username != null ? String(o.username) : null,
|
|
1842
|
+
sessionId: meta.sessionId != null ? String(meta.sessionId) : undefined,
|
|
1843
|
+
});
|
|
1844
|
+
if (meta.setContextOnly === true)
|
|
1845
|
+
return { ok: true };
|
|
1846
|
+
}
|
|
1847
|
+
const name = meta.name != null
|
|
1848
|
+
? String(meta.name)
|
|
1849
|
+
: o.type != null
|
|
1850
|
+
? String(o.type)
|
|
1851
|
+
: "event";
|
|
1852
|
+
const durationRaw = typeof o.durationMs === "number"
|
|
1853
|
+
? o.durationMs
|
|
1854
|
+
: typeof meta.durationMs === "number"
|
|
1855
|
+
? meta.durationMs
|
|
1856
|
+
: undefined;
|
|
1857
|
+
recordFleetAction({
|
|
1858
|
+
type: String(o.type || name),
|
|
1859
|
+
name,
|
|
1860
|
+
category: meta.category != null ? String(meta.category) : "ui",
|
|
1861
|
+
label: String(o.label || name),
|
|
1862
|
+
path: o.path != null ? String(o.path) : undefined,
|
|
1863
|
+
referrerPath: meta.referrerPath != null ? String(meta.referrerPath) : undefined,
|
|
1864
|
+
userId: o.userId != null ? String(o.userId) : undefined,
|
|
1865
|
+
username: o.username != null ? String(o.username) : undefined,
|
|
1866
|
+
sessionId: meta.sessionId != null ? String(meta.sessionId) : undefined,
|
|
1867
|
+
surface: meta.surface === "supplier_tab" ||
|
|
1868
|
+
meta.surface === "hermes" ||
|
|
1869
|
+
meta.surface === "n8n" ||
|
|
1870
|
+
meta.surface === "ai_workspace" ||
|
|
1871
|
+
meta.surface === "system"
|
|
1872
|
+
? meta.surface
|
|
1873
|
+
: "crm",
|
|
1874
|
+
durationMs: typeof durationRaw === "number" && Number.isFinite(durationRaw)
|
|
1875
|
+
? durationRaw
|
|
1876
|
+
: undefined,
|
|
1877
|
+
meta,
|
|
1878
|
+
});
|
|
1879
|
+
return { ok: true };
|
|
1880
|
+
});
|
|
1881
|
+
hostOnlyHandle("fleet:set-telemetry", (_e, raw) => {
|
|
1882
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
1883
|
+
const patch = {};
|
|
1884
|
+
if (typeof o.enabled === "boolean")
|
|
1885
|
+
patch.enabled = o.enabled;
|
|
1886
|
+
if (o.preset === "basic" || o.preset === "off" || o.preset === "keep") {
|
|
1887
|
+
patch.preset = o.preset;
|
|
1888
|
+
}
|
|
1889
|
+
if (o.scopes && typeof o.scopes === "object") {
|
|
1890
|
+
patch.scopes = o.scopes;
|
|
1891
|
+
}
|
|
1892
|
+
const next = deps.store().setFleetTelemetry(patch);
|
|
1893
|
+
deps.hosts.fleetAgent().notifyFleetConfigChanged();
|
|
1894
|
+
return next;
|
|
1895
|
+
});
|
|
1896
|
+
safeHandle("plugins:resolve-panel", (_e, rawId) => {
|
|
1897
|
+
const id = String(rawId || "").trim();
|
|
1898
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
1899
|
+
return { ok: false, error: "Plugins gérés par l'app Serveur" };
|
|
1900
|
+
}
|
|
1901
|
+
return deps.hosts.plugins().resolvePluginPanel(id);
|
|
1902
|
+
});
|
|
1903
|
+
hostOnlyHandle("plugins:accept-check", async (_e, rawId) => {
|
|
1904
|
+
const id = String(rawId || "").trim();
|
|
1905
|
+
return deps.hosts.pluginAccept().runPluginAcceptCheck(id);
|
|
1906
|
+
});
|
|
1907
|
+
hostOnlyHandle("embed-env:set", async (_e, rawService, rawValues) => {
|
|
1908
|
+
const service = String(rawService || "");
|
|
1909
|
+
if (!deps.vertical.isEmbedEnvService(service)) {
|
|
1910
|
+
throw new Error(`service env inconnu: ${service}`);
|
|
1911
|
+
}
|
|
1912
|
+
const values = rawValues && typeof rawValues === "object"
|
|
1913
|
+
? rawValues
|
|
1914
|
+
: {};
|
|
1915
|
+
const saved = deps.store().setEmbedUserEnv(service, values);
|
|
1916
|
+
let restarted = false;
|
|
1917
|
+
let detail = "Variables enregistrées.";
|
|
1918
|
+
if (activeConnectionProfile.mode === "local") {
|
|
1919
|
+
try {
|
|
1920
|
+
if (service === "n8n") {
|
|
1921
|
+
deps.hosts.n8n().stopN8n();
|
|
1922
|
+
const started = await deps.hosts.n8n().startN8n({
|
|
1923
|
+
connectionMode: "local",
|
|
1924
|
+
n8nConfig: deps.store().getN8nEmbedConfig(),
|
|
1925
|
+
autoBootstrap: false,
|
|
1926
|
+
onLog: (line) => log("n8n", line),
|
|
1927
|
+
});
|
|
1928
|
+
if (started)
|
|
1929
|
+
n8n = started;
|
|
1930
|
+
restarted = true;
|
|
1931
|
+
detail = "Variables enregistrées — n8n redémarré.";
|
|
1932
|
+
}
|
|
1933
|
+
else if (service === "hermes") {
|
|
1934
|
+
await deps.hosts.hermes().stopHermesAndWait();
|
|
1935
|
+
const started = await deps.hosts.hermes().startHermes({
|
|
1936
|
+
connectionMode: "local",
|
|
1937
|
+
hermesConfig: deps.store().getHermesEmbedConfig(),
|
|
1938
|
+
autoBootstrap: false,
|
|
1939
|
+
onLog: (line) => log("hermes", line),
|
|
1940
|
+
});
|
|
1941
|
+
if (started)
|
|
1942
|
+
hermes = started;
|
|
1943
|
+
await maybeRestartNextAfterHermesSpawn(Boolean(started));
|
|
1944
|
+
restarted = true;
|
|
1945
|
+
detail = "Variables enregistrées — Hermes redémarré.";
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
catch (e) {
|
|
1949
|
+
logError("embed-env", e);
|
|
1950
|
+
detail =
|
|
1951
|
+
e instanceof Error
|
|
1952
|
+
? `Enregistré, redémarrage échoué : ${e.message}`
|
|
1953
|
+
: "Enregistré, redémarrage échoué.";
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return {
|
|
1957
|
+
ok: true,
|
|
1958
|
+
service: service,
|
|
1959
|
+
values: saved,
|
|
1960
|
+
restarted,
|
|
1961
|
+
detail,
|
|
1962
|
+
panel: deps.vertical.buildEmbedEnvPanel({
|
|
1963
|
+
service,
|
|
1964
|
+
userOverlay: saved,
|
|
1965
|
+
}),
|
|
1966
|
+
};
|
|
1967
|
+
});
|
|
1968
|
+
safeHandle("config:llm-status", () => llmStatusPayload());
|
|
1969
|
+
hostOnlyHandle("config:set-llm-key", async (_e, rawArgs) => {
|
|
1970
|
+
const args = rawArgs;
|
|
1971
|
+
const key = typeof args.key === "string" ? args.key.trim() || null : null;
|
|
1972
|
+
deps.store().setLlmKey(args.provider, key);
|
|
1973
|
+
log("main", `clé BYOK ${args.provider} ${key ? "enregistrée" : "supprimée"} — propagation Next + Hermes…`);
|
|
1974
|
+
// Hermes lit OPENAI_API_KEY au spawn (.env + process) — restart obligatoire.
|
|
1975
|
+
if (activeConnectionProfile.mode === "local") {
|
|
1976
|
+
try {
|
|
1977
|
+
const hermesSync = await deps.hosts.hermes().reapplyHermesLlmKeys({
|
|
1978
|
+
connectionMode: "local",
|
|
1979
|
+
crmPort: server?.port ?? null,
|
|
1980
|
+
onLog: (line) => log("hermes", line),
|
|
1981
|
+
});
|
|
1982
|
+
if (hermesSync.restarted) {
|
|
1983
|
+
hermes = deps.hosts.hermes().getRunningHermes() ?? hermes;
|
|
1984
|
+
}
|
|
1985
|
+
log("main", `BYOK→Hermes: ${hermesSync.detail}`);
|
|
1986
|
+
}
|
|
1987
|
+
catch (e) {
|
|
1988
|
+
logError("hermes-byok", e);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
// Env Next figée au spawn : redémarrage propre obligatoire pour activer la clé.
|
|
1992
|
+
const result = await restartNextServerForLlm();
|
|
1993
|
+
return result;
|
|
1994
|
+
});
|
|
1995
|
+
safeHandle("tunnel:status", () => {
|
|
1996
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
1997
|
+
// Client léger : jamais de cloudflared local — payload inerte stable.
|
|
1998
|
+
return {
|
|
1999
|
+
configured: false,
|
|
2000
|
+
slug: null,
|
|
2001
|
+
hostname: null,
|
|
2002
|
+
publicUrl: null,
|
|
2003
|
+
publicUrls: null,
|
|
2004
|
+
online: false,
|
|
2005
|
+
error: null,
|
|
2006
|
+
pcMustBeOn: true,
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
return deps.hosts.tunnel().getTunnelStatus();
|
|
2010
|
+
});
|
|
2011
|
+
hostOnlyHandle("tunnel:check-slug", async (_e, rawSlug) => {
|
|
2012
|
+
const slug = String(rawSlug || "");
|
|
2013
|
+
return deps.hosts.tunnel().checkTunnelSlug(slug);
|
|
2014
|
+
});
|
|
2015
|
+
hostOnlyHandle("tunnel:reserve", async (_e, rawSlug) => {
|
|
2016
|
+
const slug = String(rawSlug || "");
|
|
2017
|
+
const port = server?.port ?? Number(process.env[`${deps.envPrefix}_DESKTOP_PORT`] || deps.defaultDesktopPort);
|
|
2018
|
+
const r = await deps.hosts.tunnel().reserveTunnel(slug, port);
|
|
2019
|
+
if (!r.ok)
|
|
2020
|
+
return r;
|
|
2021
|
+
try {
|
|
2022
|
+
await syncTunnelIngress();
|
|
2023
|
+
await deps.hosts.tunnel().startCloudflared();
|
|
2024
|
+
await syncN8nWebhookPublicUrl();
|
|
2025
|
+
}
|
|
2026
|
+
catch (e) {
|
|
2027
|
+
return {
|
|
2028
|
+
ok: false,
|
|
2029
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
// Aligner APP_PUBLIC_URL / MCP_PUBLIC_URL (issuer OAuth MCP) sur le slug.
|
|
2033
|
+
const restarted = await restartNextServer({ reload: false });
|
|
2034
|
+
if (!restarted.ok) {
|
|
2035
|
+
return {
|
|
2036
|
+
ok: false,
|
|
2037
|
+
error: restarted.error ||
|
|
2038
|
+
"Tunnel réservé, mais redémarrage serveur impossible (URL MCP non alignée)",
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
return r;
|
|
2042
|
+
});
|
|
2043
|
+
hostOnlyHandle("tunnel:start", async () => {
|
|
2044
|
+
if (server)
|
|
2045
|
+
await syncTunnelIngress();
|
|
2046
|
+
await deps.hosts.tunnel().startCloudflared();
|
|
2047
|
+
await syncN8nWebhookPublicUrl();
|
|
2048
|
+
return deps.hosts.tunnel().getTunnelStatus();
|
|
2049
|
+
});
|
|
2050
|
+
hostOnlyHandle("tunnel:stop", () => {
|
|
2051
|
+
deps.hosts.tunnel().stopCloudflared();
|
|
2052
|
+
return deps.hosts.tunnel().getTunnelStatus();
|
|
2053
|
+
});
|
|
2054
|
+
safeHandle("oauth:google-login", () => deps.vertical.googleLoginLoopback());
|
|
2055
|
+
safeHandle("setup:status", () => deps.store().getSetupDraft());
|
|
2056
|
+
safeHandle("setup:generate-recovery-key", () => ({
|
|
2057
|
+
recoveryKey: deps.store().generateRecoveryKey(),
|
|
2058
|
+
}));
|
|
2059
|
+
hostOnlyHandle("setup:complete", async (_e, rawArgs) => {
|
|
2060
|
+
const args = (rawArgs ?? {});
|
|
2061
|
+
const username = String(args.username || "").trim();
|
|
2062
|
+
const password = String(args.password || "");
|
|
2063
|
+
const openaiKey = String(args.openaiKey || "").trim();
|
|
2064
|
+
const slug = String(args.slug || "").trim().toLowerCase();
|
|
2065
|
+
const recoveryKey = String(args.recoveryKey || "").trim();
|
|
2066
|
+
if (!username || !password) {
|
|
2067
|
+
return { ok: false, error: "Identifiant et mot de passe requis" };
|
|
2068
|
+
}
|
|
2069
|
+
if (!recoveryKey) {
|
|
2070
|
+
return { ok: false, error: "Clé de récupération requise" };
|
|
2071
|
+
}
|
|
2072
|
+
if (!openaiKey) {
|
|
2073
|
+
return { ok: false, error: "Clé OpenAI requise" };
|
|
2074
|
+
}
|
|
2075
|
+
if (!slug) {
|
|
2076
|
+
return { ok: false, error: "Slug tunnel requis" };
|
|
2077
|
+
}
|
|
2078
|
+
if (!server) {
|
|
2079
|
+
return { ok: false, error: "Serveur local non démarré" };
|
|
2080
|
+
}
|
|
2081
|
+
try {
|
|
2082
|
+
// 1) Tunnel d'abord (réseau) — si échec, aucun compte écrit.
|
|
2083
|
+
// Reprise après crash : réutiliser le tunnel déjà réservé pour ce slug.
|
|
2084
|
+
const port = server.port;
|
|
2085
|
+
const existingTunnel = deps.store().getTunnelConfig();
|
|
2086
|
+
let reserved;
|
|
2087
|
+
if (existingTunnel && existingTunnel.slug === slug) {
|
|
2088
|
+
reserved = {
|
|
2089
|
+
ok: true,
|
|
2090
|
+
hostname: existingTunnel.hostname,
|
|
2091
|
+
publicUrl: existingTunnel.publicUrl,
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
reserved = await deps.hosts.tunnel().reserveTunnel(slug, port);
|
|
2096
|
+
}
|
|
2097
|
+
if (!reserved.ok) {
|
|
2098
|
+
return { ok: false, error: reserved.error || "Réservation tunnel impossible" };
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
await syncTunnelIngress();
|
|
2102
|
+
await deps.hosts.tunnel().startCloudflared();
|
|
2103
|
+
await syncN8nWebhookPublicUrl();
|
|
2104
|
+
}
|
|
2105
|
+
catch (e) {
|
|
2106
|
+
logError("tunnel", e);
|
|
2107
|
+
// Non bloquant : le tunnel peut être relancé depuis Configuration.
|
|
2108
|
+
}
|
|
2109
|
+
// 2) Compte + recovery + OpenAI + flags — écriture atomique.
|
|
2110
|
+
deps.store().applyFirstRunSetup({
|
|
2111
|
+
username,
|
|
2112
|
+
password,
|
|
2113
|
+
openaiKey,
|
|
2114
|
+
recoveryKey,
|
|
2115
|
+
stayLoggedIn: args.stayLoggedIn !== false,
|
|
2116
|
+
});
|
|
2117
|
+
// features.onboarding=false (demo-app) → home ; absent/true → /onboarding.
|
|
2118
|
+
const postSetupHref = isFeatureEnabled(deps.manifest, "onboarding")
|
|
2119
|
+
? "/onboarding"
|
|
2120
|
+
: "/";
|
|
2121
|
+
const restarted = await restartNextServer({
|
|
2122
|
+
forceAutoLogin: true,
|
|
2123
|
+
navigateTo: postSetupHref,
|
|
2124
|
+
reload: true,
|
|
2125
|
+
});
|
|
2126
|
+
if (!restarted.ok) {
|
|
2127
|
+
return {
|
|
2128
|
+
ok: false,
|
|
2129
|
+
error: restarted.error || "Redémarrage serveur échoué",
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
return {
|
|
2133
|
+
ok: true,
|
|
2134
|
+
hostname: reserved.hostname,
|
|
2135
|
+
publicUrl: reserved.publicUrl,
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
catch (e) {
|
|
2139
|
+
return {
|
|
2140
|
+
ok: false,
|
|
2141
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
safeHandle("config:account", () => ({
|
|
2146
|
+
...deps.store().getAccountPublic(),
|
|
2147
|
+
hasRecoveryKey: deps.store().hasRecoveryKeyConfigured(),
|
|
2148
|
+
}));
|
|
2149
|
+
// Host-only : mute le compte propriétaire LOCAL — sur un client léger le
|
|
2150
|
+
// compte vit sur le serveur distant, muter la config locale n'a aucun sens.
|
|
2151
|
+
hostOnlyHandle("config:change-password", async (_e, rawArgs) => {
|
|
2152
|
+
const args = (rawArgs ?? {});
|
|
2153
|
+
try {
|
|
2154
|
+
deps.store().changeLocalPassword(String(args.currentPassword || ""), String(args.newPassword || ""));
|
|
2155
|
+
const restarted = await restartNextServer({
|
|
2156
|
+
forceAutoLogin: deps.store().shouldAutoLoginOnBoot(),
|
|
2157
|
+
reload: true,
|
|
2158
|
+
});
|
|
2159
|
+
if (!restarted.ok) {
|
|
2160
|
+
return {
|
|
2161
|
+
ok: false,
|
|
2162
|
+
error: restarted.error || "Mot de passe enregistré, mais redémarrage échoué",
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
return { ok: true };
|
|
2166
|
+
}
|
|
2167
|
+
catch (e) {
|
|
2168
|
+
return {
|
|
2169
|
+
ok: false,
|
|
2170
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
hostOnlyHandle("auth:recover-password", async (_e, rawArgs) => {
|
|
2175
|
+
const args = (rawArgs ?? {});
|
|
2176
|
+
try {
|
|
2177
|
+
const r = deps.store().resetPasswordWithRecoveryKey(String(args.recoveryKey || ""), String(args.newPassword || ""));
|
|
2178
|
+
const restarted = await restartNextServer({
|
|
2179
|
+
forceAutoLogin: false,
|
|
2180
|
+
navigateTo: "/login",
|
|
2181
|
+
reload: true,
|
|
2182
|
+
});
|
|
2183
|
+
if (!restarted.ok) {
|
|
2184
|
+
return {
|
|
2185
|
+
ok: false,
|
|
2186
|
+
error: restarted.error || "Mot de passe réinitialisé, mais redémarrage échoué",
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
return { ok: true, username: r.username };
|
|
2190
|
+
}
|
|
2191
|
+
catch (e) {
|
|
2192
|
+
return {
|
|
2193
|
+
ok: false,
|
|
2194
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
hostOnlyHandle("search:reindex", async () => {
|
|
2199
|
+
if (!meili) {
|
|
2200
|
+
return { ok: false, error: "Meilisearch indisponible" };
|
|
2201
|
+
}
|
|
2202
|
+
try {
|
|
2203
|
+
log("main", "réindexation Meili demandée (IPC search:reindex)");
|
|
2204
|
+
deps.vertical.setBootStage("index");
|
|
2205
|
+
const reindexStarted = Date.now();
|
|
2206
|
+
await runIndexerWithProgress(meili);
|
|
2207
|
+
const decision = await meiliIndexReady(meili);
|
|
2208
|
+
if (!decision) {
|
|
2209
|
+
return {
|
|
2210
|
+
ok: true,
|
|
2211
|
+
degraded: true,
|
|
2212
|
+
detail: "Réindexation lancée, contrôle de cohérence indisponible",
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
log("main", `meili.reindex done ready=${decision.ready} reason=${decision.reason}`);
|
|
2216
|
+
track({
|
|
2217
|
+
level: "event",
|
|
2218
|
+
kind: "index.run",
|
|
2219
|
+
outcome: decision.ready ? "ok" : "still-not-ready",
|
|
2220
|
+
reason: "manual-reindex",
|
|
2221
|
+
durationMs: Date.now() - reindexStarted,
|
|
2222
|
+
});
|
|
2223
|
+
return {
|
|
2224
|
+
ok: true,
|
|
2225
|
+
ready: decision.ready,
|
|
2226
|
+
reason: decision.reason,
|
|
2227
|
+
sql: decision.sql,
|
|
2228
|
+
meili: decision.meili,
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
catch (e) {
|
|
2232
|
+
logError("indexer", e);
|
|
2233
|
+
return {
|
|
2234
|
+
ok: false,
|
|
2235
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
hostOnlyHandle("config:factory-reset", async () => {
|
|
2240
|
+
try {
|
|
2241
|
+
log("main", "factory-reset demandé");
|
|
2242
|
+
track({ level: "event", kind: "factory.reset", outcome: "requested" });
|
|
2243
|
+
recordFleetAction({
|
|
2244
|
+
type: "system.factory_reset",
|
|
2245
|
+
name: "system.factory_reset",
|
|
2246
|
+
category: "system",
|
|
2247
|
+
label: "Factory reset demandé",
|
|
2248
|
+
surface: "system",
|
|
2249
|
+
});
|
|
2250
|
+
// Empêche le handler child-exit Next de crier pendant l'arrêt volontaire.
|
|
2251
|
+
restartingServer = true;
|
|
2252
|
+
deps.hosts.tunnel().stopCloudflared();
|
|
2253
|
+
bridge?.stop();
|
|
2254
|
+
bridge = null;
|
|
2255
|
+
try {
|
|
2256
|
+
tabs?.closeAll();
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
/* ignore */
|
|
2260
|
+
}
|
|
2261
|
+
try {
|
|
2262
|
+
assistantChrome?.setMode("hidden");
|
|
2263
|
+
}
|
|
2264
|
+
catch {
|
|
2265
|
+
/* ignore */
|
|
2266
|
+
}
|
|
2267
|
+
if (server) {
|
|
2268
|
+
try {
|
|
2269
|
+
await clearCrmSessionCookie(server.baseUrl);
|
|
2270
|
+
}
|
|
2271
|
+
catch {
|
|
2272
|
+
/* ignore */
|
|
2273
|
+
}
|
|
2274
|
+
const old = server;
|
|
2275
|
+
server = null;
|
|
2276
|
+
old.stop();
|
|
2277
|
+
}
|
|
2278
|
+
if (meili) {
|
|
2279
|
+
try {
|
|
2280
|
+
meili.stop();
|
|
2281
|
+
}
|
|
2282
|
+
catch {
|
|
2283
|
+
/* ignore */
|
|
2284
|
+
}
|
|
2285
|
+
meili = null;
|
|
2286
|
+
}
|
|
2287
|
+
// Embeds AVANT wipe : hermes-home / n8n-home / runtimes sont dans les
|
|
2288
|
+
// cibles — un process encore vivant verrouille les fichiers (Windows).
|
|
2289
|
+
try {
|
|
2290
|
+
deps.hosts.plugins().stopAllPlugins();
|
|
2291
|
+
}
|
|
2292
|
+
catch {
|
|
2293
|
+
/* ignore */
|
|
2294
|
+
}
|
|
2295
|
+
try {
|
|
2296
|
+
deps.hosts.n8n().stopN8n();
|
|
2297
|
+
}
|
|
2298
|
+
catch {
|
|
2299
|
+
/* ignore */
|
|
2300
|
+
}
|
|
2301
|
+
try {
|
|
2302
|
+
await deps.hosts.hermes().stopHermesAndWait();
|
|
2303
|
+
}
|
|
2304
|
+
catch {
|
|
2305
|
+
/* ignore */
|
|
2306
|
+
}
|
|
2307
|
+
hermes = null;
|
|
2308
|
+
n8n = null;
|
|
2309
|
+
const { wiped } = await deps.hosts.factoryReset().wipeLocalUserData();
|
|
2310
|
+
log("main", `factory-reset : ${wiped.length} cibles effacées`);
|
|
2311
|
+
track({
|
|
2312
|
+
level: "event",
|
|
2313
|
+
kind: "factory.reset",
|
|
2314
|
+
outcome: "done",
|
|
2315
|
+
ctx: { wiped },
|
|
2316
|
+
});
|
|
2317
|
+
restartingServer = false;
|
|
2318
|
+
// Relance : picker connexion (config effacée) puis first-run local si choisi.
|
|
2319
|
+
activeCrmBaseUrl = null;
|
|
2320
|
+
deps.store().markConnectionPickerRequired();
|
|
2321
|
+
activeConnectionProfile = { mode: "local", localBind: "127.0.0.1", chosen: false };
|
|
2322
|
+
if (appView && !appView.webContents.isDestroyed()) {
|
|
2323
|
+
await appView.webContents.loadURL(splashHtmlUrl()).catch(() => { });
|
|
2324
|
+
void (async () => {
|
|
2325
|
+
try {
|
|
2326
|
+
await ensureConnectionChosen(appView);
|
|
2327
|
+
await setupAndStart(appView);
|
|
2328
|
+
}
|
|
2329
|
+
catch (e) {
|
|
2330
|
+
logError("main", e);
|
|
2331
|
+
void appView
|
|
2332
|
+
.webContents.loadURL(errorHtml("Échec après remise à zéro", e instanceof Error ? e.message : String(e)))
|
|
2333
|
+
.catch(() => { });
|
|
2334
|
+
}
|
|
2335
|
+
})();
|
|
2336
|
+
}
|
|
2337
|
+
return { ok: true };
|
|
2338
|
+
}
|
|
2339
|
+
catch (e) {
|
|
2340
|
+
restartingServer = false;
|
|
2341
|
+
return {
|
|
2342
|
+
ok: false,
|
|
2343
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
safeHandle("auth:set-stay-logged-in", (_e, rawStay) => {
|
|
2348
|
+
deps.store().setStayLoggedIn(Boolean(rawStay));
|
|
2349
|
+
return { ok: true, stayLoggedIn: Boolean(rawStay) };
|
|
2350
|
+
});
|
|
2351
|
+
safeHandle("auth:logout", async () => {
|
|
2352
|
+
deps.store().setSkipAutoLogin(true);
|
|
2353
|
+
const base = crmBaseUrl();
|
|
2354
|
+
if (base) {
|
|
2355
|
+
await clearCrmSessionCookie(base);
|
|
2356
|
+
}
|
|
2357
|
+
return { ok: true };
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
/* ─────────────────────────────── Boot ──────────────────────────────────── */
|
|
2361
|
+
/** Coquille : fenêtre + vue CRM + gestion onglets + IPC (créée UNE fois). */
|
|
2362
|
+
async function createShell() {
|
|
2363
|
+
// Windows : frameless façon Notion (boutons min/max/close dans le header onglets).
|
|
2364
|
+
// Autres plateformes : cadre natif + menu application masqué.
|
|
2365
|
+
const framelessWin = process.platform === "win32";
|
|
2366
|
+
win = new BaseWindow({
|
|
2367
|
+
width: 1400,
|
|
2368
|
+
height: 900,
|
|
2369
|
+
minWidth: 1000,
|
|
2370
|
+
minHeight: 640,
|
|
2371
|
+
title: deps.appKind === "server" ? productNameServer : productName,
|
|
2372
|
+
backgroundColor: "#14182f",
|
|
2373
|
+
autoHideMenuBar: true,
|
|
2374
|
+
// Linux/XFCE/xrdp : décorations natives explicites (X / Alt+F4).
|
|
2375
|
+
closable: true,
|
|
2376
|
+
minimizable: true,
|
|
2377
|
+
maximizable: true,
|
|
2378
|
+
...(framelessWin
|
|
2379
|
+
? { frame: false, thickFrame: true }
|
|
2380
|
+
: { frame: true }),
|
|
2381
|
+
});
|
|
2382
|
+
const w = win;
|
|
2383
|
+
// Preload unique `preload.js` (H10 — le basename historique des clients
|
|
2384
|
+
// legacy n'est plus sonde ; migration via codemod scripts/codemods/H10).
|
|
2385
|
+
// Un preload manquant/incassable = plus d'API desktop → on veut le SAVOIR.
|
|
2386
|
+
const appPreload = deps.paths.preloadPath("preload.js");
|
|
2387
|
+
if (!fs.existsSync(appPreload)) {
|
|
2388
|
+
deps.vertical.reportCrash("web-event", { view: "crm", event: "preload-missing", preloadPath: appPreload });
|
|
2389
|
+
}
|
|
2390
|
+
const view = new WebContentsView({
|
|
2391
|
+
webPreferences: {
|
|
2392
|
+
partition: deps.sessionPartition,
|
|
2393
|
+
...(fs.existsSync(appPreload) ? { preload: appPreload } : {}),
|
|
2394
|
+
contextIsolation: true,
|
|
2395
|
+
sandbox: true,
|
|
2396
|
+
nodeIntegration: false,
|
|
2397
|
+
},
|
|
2398
|
+
});
|
|
2399
|
+
appView = view;
|
|
2400
|
+
w.contentView.addChildView(view);
|
|
2401
|
+
const fit = () => {
|
|
2402
|
+
const { width, height } = w.getContentBounds();
|
|
2403
|
+
view.setBounds({ x: 0, y: 0, width, height });
|
|
2404
|
+
};
|
|
2405
|
+
fit();
|
|
2406
|
+
w.on("resize", fit);
|
|
2407
|
+
// Fermer = masquer (tray) : l'app et le serveur restent actifs en arrière-plan
|
|
2408
|
+
// (collaborateurs IA joignables via MCP/API fenêtre fermée). Le vrai quit
|
|
2409
|
+
// passe par le menu tray (flag quitting) ou before-quit. Si le tray n'a pas
|
|
2410
|
+
// pu être créé, on garde le comportement historique : fermer = quitter.
|
|
2411
|
+
installCloseToTray(w, {
|
|
2412
|
+
productName: deps.manifest.client.productName,
|
|
2413
|
+
trayActive: () => trayController?.active ?? false,
|
|
2414
|
+
isQuitting: () => quitting,
|
|
2415
|
+
closeToTrayEnabled: () => deps.store().getBackgroundSettings().closeToTray,
|
|
2416
|
+
// Boot raté / splash : quitter vraiment (évite fenêtre grise zombie sous RDP).
|
|
2417
|
+
forceQuitOnClose: () => !bootReady,
|
|
2418
|
+
});
|
|
2419
|
+
// Fermeture RÉELLE de la fenêtre principale (tray inactif / closeToTray
|
|
2420
|
+
// désactivé) : les fenêtres IA masquées ne doivent pas maintenir l'app en
|
|
2421
|
+
// vie (window-all-closed ne se déclencherait jamais).
|
|
2422
|
+
w.on("closed", () => {
|
|
2423
|
+
if (!quitting) {
|
|
2424
|
+
quitting = true;
|
|
2425
|
+
app.quit();
|
|
2426
|
+
}
|
|
2427
|
+
});
|
|
2428
|
+
// Sync état maximisé → UI (icône restore / maximize).
|
|
2429
|
+
const emitMaximized = () => {
|
|
2430
|
+
try {
|
|
2431
|
+
if (!view.webContents.isDestroyed()) {
|
|
2432
|
+
view.webContents.send("window:maximized-changed", w.isMaximized());
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
catch (e) {
|
|
2436
|
+
logError("window", e);
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
w.on("maximize", emitMaximized);
|
|
2440
|
+
w.on("unmaximize", emitMaximized);
|
|
2441
|
+
// Télémétrie complète de la vue CRM : render-process-gone, preload-error,
|
|
2442
|
+
// did-fail-load, unresponsive, console-message error…
|
|
2443
|
+
deps.vertical.instrumentWebContents(view.webContents, "crm");
|
|
2444
|
+
// Activité flotte : navigations CRM (opt-in côté consentement à l’envoi).
|
|
2445
|
+
// Navigations CRM aussi capturées côté renderer (page.view + dwell).
|
|
2446
|
+
// Ici : filet de sécurité si le tracker React n’est pas monté.
|
|
2447
|
+
let lastFleetNavPath = "";
|
|
2448
|
+
const trackCrmNav = (url) => {
|
|
2449
|
+
try {
|
|
2450
|
+
const u = new URL(url);
|
|
2451
|
+
const p = `${u.pathname}${u.search}`.slice(0, 300);
|
|
2452
|
+
if (!p || p === lastFleetNavPath)
|
|
2453
|
+
return;
|
|
2454
|
+
lastFleetNavPath = p;
|
|
2455
|
+
recordFleetAction({
|
|
2456
|
+
type: "page.view",
|
|
2457
|
+
name: "page.view",
|
|
2458
|
+
category: "navigation",
|
|
2459
|
+
label: `Page ${u.pathname}`,
|
|
2460
|
+
path: p,
|
|
2461
|
+
surface: "crm",
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
catch {
|
|
2465
|
+
/* ignore */
|
|
2466
|
+
}
|
|
2467
|
+
};
|
|
2468
|
+
view.webContents.on("did-navigate", (_e, url) => trackCrmNav(url));
|
|
2469
|
+
view.webContents.on("did-navigate-in-page", (_e, url) => trackCrmNav(url));
|
|
2470
|
+
// Crash renderer (UI CRM) : rechargement automatique (le rapport est émis
|
|
2471
|
+
// par deps.vertical.instrumentWebContents).
|
|
2472
|
+
view.webContents.on("render-process-gone", (_e, details) => {
|
|
2473
|
+
if (details.reason !== "clean-exit" && server) {
|
|
2474
|
+
setTimeout(() => {
|
|
2475
|
+
if (!view.webContents.isDestroyed()) {
|
|
2476
|
+
void view.webContents.loadURL(server.baseUrl).catch(() => { });
|
|
2477
|
+
}
|
|
2478
|
+
}, 1000);
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
await view.webContents.loadURL(splashHtmlUrl());
|
|
2482
|
+
tabs = deps.vertical.createSupplierTabs(w, view);
|
|
2483
|
+
// Workspaces IA : fenêtres parallèles persistantes par défaut (Q1/Q8) ;
|
|
2484
|
+
// réglage local-config "embedded" = compat « Voir comme IA » historique.
|
|
2485
|
+
aiWorkspaces = deps.vertical.createAiWorkspaces(w, view, tabs, {
|
|
2486
|
+
defaultPresentation: () => deps.store().getAiWorkspacePresentation(),
|
|
2487
|
+
isQuitting: () => quitting,
|
|
2488
|
+
onWindowsChanged: () => trayController?.refresh(),
|
|
2489
|
+
});
|
|
2490
|
+
assistantChrome = deps.vertical.createAssistantChrome(w, () => {
|
|
2491
|
+
try {
|
|
2492
|
+
if (!view.webContents.isDestroyed()) {
|
|
2493
|
+
view.webContents.send("assistant:open-request");
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
catch (e) {
|
|
2497
|
+
logError("assistant-chrome", e);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
registerIpc(view);
|
|
2501
|
+
return view;
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Picker Héberger / Rejoindre — **toujours** avant Next/Meili/tunnel (Option A).
|
|
2505
|
+
* Dernier mode + URL pré-cochés ; Continuer démarre ensuite le boot choisi.
|
|
2506
|
+
* Aucun serveur local en arrière-plan pendant l’affichage du picker.
|
|
2507
|
+
* Pas de case bind LAN ici : tunnel = accès distant (config serveur / onboarding).
|
|
2508
|
+
*/
|
|
2509
|
+
async function ensureConnectionChosen(view) {
|
|
2510
|
+
const safeFallback = defaultProfileForAppKind(deps.appKind);
|
|
2511
|
+
// App Serveur : pas de picker — la stack locale démarre directement
|
|
2512
|
+
// (le splash de progression prend le relais tout de suite).
|
|
2513
|
+
if (deps.bootBehavior.forceLocalProfile) {
|
|
2514
|
+
const stored = deps.store().getConnectionProfile?.() ?? null;
|
|
2515
|
+
const bind = stored && typeof stored === "object" && stored.localBind === "0.0.0.0"
|
|
2516
|
+
? "0.0.0.0"
|
|
2517
|
+
: "127.0.0.1";
|
|
2518
|
+
const direct = {
|
|
2519
|
+
mode: "local",
|
|
2520
|
+
localBind: bind,
|
|
2521
|
+
chosen: true,
|
|
2522
|
+
};
|
|
2523
|
+
activeConnectionProfile = direct;
|
|
2524
|
+
try {
|
|
2525
|
+
deps.store().setConnectionProfile(direct);
|
|
2526
|
+
}
|
|
2527
|
+
catch (e) {
|
|
2528
|
+
logError("main", e);
|
|
2529
|
+
}
|
|
2530
|
+
log("main", "app kind=server → boot local direct (picker sauté)");
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
// Profil join (argv --tf2-profile=join: ou deep-link tempoflow://join/…) :
|
|
2534
|
+
// le serveur cible est déjà connu → picker sauté, connexion directe.
|
|
2535
|
+
const launch = deps.bootProfileLaunch;
|
|
2536
|
+
if (launch && launch.mode === "join" && launch.serverUrl) {
|
|
2537
|
+
const direct = deps.vertical.assertProfileReady({
|
|
2538
|
+
mode: "remote",
|
|
2539
|
+
remoteUrl: launch.serverUrl,
|
|
2540
|
+
chosen: true,
|
|
2541
|
+
});
|
|
2542
|
+
activeConnectionProfile = direct;
|
|
2543
|
+
try {
|
|
2544
|
+
deps.store().setConnectionProfile(direct);
|
|
2545
|
+
deps.store().rememberServer(direct.remoteUrl || "");
|
|
2546
|
+
}
|
|
2547
|
+
catch (e) {
|
|
2548
|
+
logError("main", e);
|
|
2549
|
+
}
|
|
2550
|
+
log("main", `profil join direct → ${direct.remoteUrl} — picker sauté`);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
let resolved;
|
|
2554
|
+
try {
|
|
2555
|
+
resolved = deps.vertical.resolveBootProfile(deps.store().getConnectionProfileStored?.() ?? null);
|
|
2556
|
+
}
|
|
2557
|
+
catch (e) {
|
|
2558
|
+
logError("main", e);
|
|
2559
|
+
resolved = null;
|
|
2560
|
+
}
|
|
2561
|
+
// Compat stubs marque : `{ profile, showPicker }` OU profil nu OU undefined.
|
|
2562
|
+
const { profile, showPicker } = unwrapBootProfileResult(resolved, deps.appKind);
|
|
2563
|
+
if (!showPicker) {
|
|
2564
|
+
// Garde-fou : deps.vertical.resolveBootProfile doit toujours renvoyer true (pas de skip silencieux).
|
|
2565
|
+
log("main", "WARN deps.vertical.resolveBootProfile.showPicker=false — forçage picker");
|
|
2566
|
+
}
|
|
2567
|
+
deps.vertical.setBootStage("connection-picker");
|
|
2568
|
+
const joinOnly = deps.bootBehavior.pickerVariant === "join-only";
|
|
2569
|
+
setSplashStatus(joinOnly ? "Choix du serveur à rejoindre…" : "Choix : héberger ou rejoindre…");
|
|
2570
|
+
let last;
|
|
2571
|
+
try {
|
|
2572
|
+
last = deps.vertical.sanitizeConnectionProfile?.(profile);
|
|
2573
|
+
}
|
|
2574
|
+
catch (e) {
|
|
2575
|
+
logError("main", e);
|
|
2576
|
+
last = null;
|
|
2577
|
+
}
|
|
2578
|
+
if (!last || typeof last !== "object" || !last.mode) {
|
|
2579
|
+
last = { ...safeFallback };
|
|
2580
|
+
}
|
|
2581
|
+
const localSetupDone = deps.store().isSetupComplete();
|
|
2582
|
+
let recallLine = "";
|
|
2583
|
+
if (joinOnly) {
|
|
2584
|
+
recallLine =
|
|
2585
|
+
last.mode === "remote" && last.remoteUrl
|
|
2586
|
+
? `Dernier serveur rejoint : ${last.remoteUrl}. Confirmez ou changez, puis Continuer.`
|
|
2587
|
+
: `Cette app est le client ${productName} : elle se connecte à un serveur (app ${productNameServer} ou hôte du cabinet).`;
|
|
2588
|
+
}
|
|
2589
|
+
else if (last.mode === "remote" && last.remoteUrl) {
|
|
2590
|
+
recallLine = `Dernier choix : rejoindre ${last.remoteUrl}. Confirmez ou changez, puis Continuer.`;
|
|
2591
|
+
}
|
|
2592
|
+
else if (last.chosen && localSetupDone) {
|
|
2593
|
+
recallLine =
|
|
2594
|
+
"Dernier choix : héberger le serveur local sur ce PC. Confirmez ou rejoignez un autre serveur.";
|
|
2595
|
+
}
|
|
2596
|
+
else if (localSetupDone) {
|
|
2597
|
+
recallLine =
|
|
2598
|
+
"Un serveur local est déjà configuré sur ce PC. Choisissez « Héberger » pour le reprendre, ou « Rejoindre » un autre serveur.";
|
|
2599
|
+
}
|
|
2600
|
+
else {
|
|
2601
|
+
recallLine =
|
|
2602
|
+
"Premier démarrage : choisissez si ce PC héberge le cabinet, ou s’il rejoint un serveur existant.";
|
|
2603
|
+
}
|
|
2604
|
+
log("main", `picker connexion (${joinOnly ? "join-only" : "complet"}, pré-sélection ${last.mode}` +
|
|
2605
|
+
(last.mode === "remote" ? ` → ${last.remoteUrl || "?"}` : "") +
|
|
2606
|
+
") — pas de startLocalServer tant que Continuer n'est pas validé");
|
|
2607
|
+
// URL pré-provisionnée (installateur client d'un cabinet) : env prioritaire
|
|
2608
|
+
// puis manifest.defaultServerUrl — ne remplace jamais un dernier choix.
|
|
2609
|
+
const presetServerUrl = String(process.env[`${deps.envPrefix}_DEFAULT_SERVER_URL`] ||
|
|
2610
|
+
deps.manifest
|
|
2611
|
+
?.defaultServerUrl ||
|
|
2612
|
+
"").trim();
|
|
2613
|
+
await view.webContents.loadURL(deps.vertical.profilePickerHtml({
|
|
2614
|
+
initialMode: joinOnly || last.mode === "remote" ? "remote" : "local",
|
|
2615
|
+
remoteUrl: last.remoteUrl || presetServerUrl || "",
|
|
2616
|
+
localSetupDone,
|
|
2617
|
+
recallLine,
|
|
2618
|
+
rememberedServers: deps.store().listRememberedServers().map((s) => ({
|
|
2619
|
+
id: s.id,
|
|
2620
|
+
url: s.url,
|
|
2621
|
+
label: s.label,
|
|
2622
|
+
})),
|
|
2623
|
+
joinOnly,
|
|
2624
|
+
}));
|
|
2625
|
+
const chosen = await new Promise((resolve) => {
|
|
2626
|
+
connectionChoiceResolver = resolve;
|
|
2627
|
+
});
|
|
2628
|
+
activeConnectionProfile =
|
|
2629
|
+
chosen && typeof chosen === "object" && chosen.mode
|
|
2630
|
+
? chosen
|
|
2631
|
+
: { ...safeFallback, chosen: true };
|
|
2632
|
+
log("main", `profil connexion choisi : ${activeConnectionProfile.mode}` +
|
|
2633
|
+
(activeConnectionProfile.mode === "remote"
|
|
2634
|
+
? ` → ${activeConnectionProfile.remoteUrl}`
|
|
2635
|
+
: ` (bind ${activeConnectionProfile.localBind})`) +
|
|
2636
|
+
" — démarrage serveur / remote ensuite");
|
|
2637
|
+
await view.webContents.loadURL(splashHtmlUrl()).catch(() => { });
|
|
2638
|
+
}
|
|
2639
|
+
/** Client thin : charge l'UI depuis un serveur distant (pas de Next/Meili locaux). */
|
|
2640
|
+
/**
|
|
2641
|
+
* Garde offline du client thin : perte du serveur en cours de session →
|
|
2642
|
+
* page « Hors ligne » (retry backoff testConnection + rechooseConnection).
|
|
2643
|
+
* Idempotent (un seul listener par vue, retentable via setupAndStart).
|
|
2644
|
+
*/
|
|
2645
|
+
let remoteOfflineGuardInstalled = false;
|
|
2646
|
+
function installRemoteOfflineGuard(view, crmUrl) {
|
|
2647
|
+
if (remoteOfflineGuardInstalled)
|
|
2648
|
+
return;
|
|
2649
|
+
remoteOfflineGuardInstalled = true;
|
|
2650
|
+
view.webContents.on("did-fail-load", (_e, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
|
2651
|
+
if (!isMainFrame)
|
|
2652
|
+
return;
|
|
2653
|
+
if (errorCode === -3)
|
|
2654
|
+
return; // ERR_ABORTED (navigation remplacée)
|
|
2655
|
+
if (activeConnectionProfile.mode !== "remote")
|
|
2656
|
+
return;
|
|
2657
|
+
// Seules les pertes du CRM distant comptent (pas les data:/devtools).
|
|
2658
|
+
if (validatedURL && !validatedURL.startsWith(activeCrmBaseUrl || crmUrl)) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
log("main", `remote offline (${errorCode} ${errorDescription}) → écran reconnexion`);
|
|
2662
|
+
view.webContents
|
|
2663
|
+
.loadURL(remoteOfflineHtml({
|
|
2664
|
+
productName,
|
|
2665
|
+
bridgeName: deps.manifest.bridgeName,
|
|
2666
|
+
crmUrl: activeCrmBaseUrl || crmUrl,
|
|
2667
|
+
detail: errorDescription || undefined,
|
|
2668
|
+
}))
|
|
2669
|
+
.catch((err) => logError("main", err));
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
async function setupAndStartRemote(view) {
|
|
2673
|
+
const ready = deps.vertical.assertProfileReady(activeConnectionProfile);
|
|
2674
|
+
splashBeginRemote();
|
|
2675
|
+
deps.vertical.setBootStage("remote-health");
|
|
2676
|
+
splashGo("remote", {
|
|
2677
|
+
headline: "Connexion au serveur distant…",
|
|
2678
|
+
detail: ready.remoteUrl || "Vérification…",
|
|
2679
|
+
percent: 20,
|
|
2680
|
+
});
|
|
2681
|
+
const health = await deps.vertical.testRemoteHealth(ready.remoteUrl || "", 12000);
|
|
2682
|
+
if (!health.ok || !health.baseUrl) {
|
|
2683
|
+
splashPatch("remote", {
|
|
2684
|
+
status: "error",
|
|
2685
|
+
detail: health.error || "Serveur injoignable",
|
|
2686
|
+
});
|
|
2687
|
+
throw new Error(health.error ||
|
|
2688
|
+
`Serveur distant injoignable (${ready.remoteUrl}). Vérifiez l'URL, le réseau et le pare-feu.`);
|
|
2689
|
+
}
|
|
2690
|
+
activeCrmBaseUrl = health.baseUrl;
|
|
2691
|
+
activeConnectionProfile = { ...ready, remoteUrl: health.baseUrl };
|
|
2692
|
+
log("main", `mode remote → ${activeCrmBaseUrl}`);
|
|
2693
|
+
splashDone("remote", health.baseUrl);
|
|
2694
|
+
deps.vertical.setBootStage("load-ui");
|
|
2695
|
+
routeExternalLinksToTabs(view, activeCrmBaseUrl);
|
|
2696
|
+
splashGo("login", {
|
|
2697
|
+
headline: "Ouverture de l’interface…",
|
|
2698
|
+
detail: activeCrmBaseUrl,
|
|
2699
|
+
percent: 90,
|
|
2700
|
+
});
|
|
2701
|
+
// Cookie session = origin distante ; login via /login si pas de session.
|
|
2702
|
+
await view.webContents.loadURL(activeCrmBaseUrl);
|
|
2703
|
+
splashDone("login", "Interface chargée");
|
|
2704
|
+
// UX reconnexion : serveur KO en cours de session → écran offline
|
|
2705
|
+
// (retry auto backoff via testConnection + « Changer de serveur »).
|
|
2706
|
+
installRemoteOfflineGuard(view, activeCrmBaseUrl);
|
|
2707
|
+
// Bridge computer-use vers le serveur distant (auth session — le loop
|
|
2708
|
+
// attend le login utilisateur avec backoff si pas encore de cookie).
|
|
2709
|
+
await startRemoteBridge(activeCrmBaseUrl);
|
|
2710
|
+
deps.vertical.setBootStage("ready");
|
|
2711
|
+
setUpdaterRenderer(sendUpdateToWebContents(view.webContents));
|
|
2712
|
+
log("main", "démarrage remote terminé.");
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Séquence de démarrage complète (retentable via l'écran d'erreur) :
|
|
2716
|
+
* connexion → [local: catalogue → migrations → Meili → index → serveur → login → bridge]
|
|
2717
|
+
* | [remote: health → loadURL]
|
|
2718
|
+
*/
|
|
2719
|
+
async function setupAndStart(view) {
|
|
2720
|
+
if (activeConnectionProfile.mode === "remote") {
|
|
2721
|
+
await setupAndStartRemote(view);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
// App Client (join-only) : AUCUN code serveur local ne doit s'exécuter.
|
|
2725
|
+
// Garde-fou dur : si un profil local arrive jusqu'ici, on refuse le boot
|
|
2726
|
+
// plutôt que de démarrer Meili/Next/n8n/Hermes sur un poste client.
|
|
2727
|
+
if (!deps.bootBehavior.allowLocalStack) {
|
|
2728
|
+
throw new Error(`Cette app est le client ${productName} (join-only) : impossible d'héberger un serveur local. ` +
|
|
2729
|
+
`Installez « ${productNameServer} » pour héberger.`);
|
|
2730
|
+
}
|
|
2731
|
+
bootLocalStartedAt = Date.now();
|
|
2732
|
+
track({
|
|
2733
|
+
level: "event",
|
|
2734
|
+
kind: "boot.start",
|
|
2735
|
+
ctx: { mode: activeConnectionProfile.mode },
|
|
2736
|
+
});
|
|
2737
|
+
const hermesCfg = deps.store().getHermesEmbedConfig();
|
|
2738
|
+
const n8nCfg = deps.store().getN8nEmbedConfig();
|
|
2739
|
+
const configuredNeedHermes = deps.vertical.shouldSpawnEmbeddedHermes({
|
|
2740
|
+
connectionMode: "local",
|
|
2741
|
+
hermes: hermesCfg,
|
|
2742
|
+
});
|
|
2743
|
+
const configuredNeedN8n = deps.vertical.shouldSpawnEmbeddedN8n({
|
|
2744
|
+
connectionMode: "local",
|
|
2745
|
+
n8n: n8nCfg,
|
|
2746
|
+
});
|
|
2747
|
+
// Contrat produit TF2 : la stack native s'installe et démarre PENDANT le
|
|
2748
|
+
// splash, avec une ligne + barre + chrono par outil. L'utilisateur voit la
|
|
2749
|
+
// progression et arrive sur un CRM dont les services sont réellement
|
|
2750
|
+
// disponibles. Opt-out diagnostic : CREEZIO_EMBEDS_BLOCK_UI=0 (warm différé
|
|
2751
|
+
// après l'UI, services indisponibles au premier écran).
|
|
2752
|
+
const deferEmbedWarm = process.env.CREEZIO_EMBEDS_BLOCK_UI === "0";
|
|
2753
|
+
const needHermes = configuredNeedHermes && !deferEmbedWarm;
|
|
2754
|
+
const needN8n = configuredNeedN8n && !deferEmbedWarm;
|
|
2755
|
+
const needNode = needN8n;
|
|
2756
|
+
const needTunnel = Boolean(deps.hosts.tunnel().publicUrlForServer());
|
|
2757
|
+
splashBeginLocal({
|
|
2758
|
+
needNode,
|
|
2759
|
+
needHermes,
|
|
2760
|
+
needN8n,
|
|
2761
|
+
needTunnel,
|
|
2762
|
+
});
|
|
2763
|
+
/* 1. Catalogue (téléchargé au 1er lancement, avec progression live) */
|
|
2764
|
+
deps.vertical.setBootStage("catalog");
|
|
2765
|
+
splashGo("catalog", {
|
|
2766
|
+
headline: "Catalogue fournisseurs…",
|
|
2767
|
+
detail: "Vérification / téléchargement si besoin",
|
|
2768
|
+
percent: 5,
|
|
2769
|
+
});
|
|
2770
|
+
const catalogStarted = Date.now();
|
|
2771
|
+
const catalogState = await deps.hosts.catalog().ensureCatalogPresent((p) => {
|
|
2772
|
+
setSplashProgress(p);
|
|
2773
|
+
});
|
|
2774
|
+
splashDone("catalog", catalogState === "present" ? "Déjà présent" : "Installé");
|
|
2775
|
+
log("main", `catalogue : ${catalogState}`);
|
|
2776
|
+
trackDecision("catalog.ensure", catalogState, {
|
|
2777
|
+
durationMs: Date.now() - catalogStarted,
|
|
2778
|
+
});
|
|
2779
|
+
/* 2. Migrations — BLOQUANTES : indexer une base KO produit un Meili stale. */
|
|
2780
|
+
deps.vertical.setBootStage("migrations");
|
|
2781
|
+
splashGo("migrations", {
|
|
2782
|
+
headline: "Mise à jour de la base de données…",
|
|
2783
|
+
detail: "Migrations SQLite",
|
|
2784
|
+
percent: 30,
|
|
2785
|
+
});
|
|
2786
|
+
const migrationsStarted = Date.now();
|
|
2787
|
+
const migrationsPath = await runMigrationsInNode();
|
|
2788
|
+
splashDone("migrations", migrationsPath === "kernel"
|
|
2789
|
+
? "Migrations kernel appliquées (core + brand)"
|
|
2790
|
+
: "Migrations appliquées");
|
|
2791
|
+
track({
|
|
2792
|
+
level: "event",
|
|
2793
|
+
kind: "migrations.done",
|
|
2794
|
+
outcome: "ok",
|
|
2795
|
+
durationMs: Date.now() - migrationsStarted,
|
|
2796
|
+
ctx: { path: migrationsPath },
|
|
2797
|
+
});
|
|
2798
|
+
/* 2b. Runtime brand H6 — nominal (échec VISIBLE sur splash + crash-collector).
|
|
2799
|
+
* Fail-soft conservé pour l’UI Hono historique, mais plus jamais silencieux. */
|
|
2800
|
+
deps.vertical.setBootStage("brand-runtime");
|
|
2801
|
+
splashGo("runtime", {
|
|
2802
|
+
headline: "Runtime plateforme…",
|
|
2803
|
+
detail: "Core + brand SQLite, modules",
|
|
2804
|
+
percent: 40,
|
|
2805
|
+
});
|
|
2806
|
+
try {
|
|
2807
|
+
const brandRt = deps.vertical.bootBrandRuntime({
|
|
2808
|
+
userDataRoot: deps.paths.userDataDir(),
|
|
2809
|
+
isPackaged: deps.paths.isPackaged(),
|
|
2810
|
+
appVersion: app.getVersion(),
|
|
2811
|
+
});
|
|
2812
|
+
const mods = Array.isArray(brandRt.mountedModules)
|
|
2813
|
+
? brandRt.mountedModules
|
|
2814
|
+
: [];
|
|
2815
|
+
log("h3", `brand runtime ready — modules=[${mods.join(",")}] ` +
|
|
2816
|
+
`core=${brandRt.runtime?.paths?.core} brand=${brandRt.runtime?.paths?.brand}`);
|
|
2817
|
+
track({
|
|
2818
|
+
level: "event",
|
|
2819
|
+
kind: "h3.brand_runtime.ready",
|
|
2820
|
+
outcome: "ok",
|
|
2821
|
+
ctx: { modules: mods.join(",") },
|
|
2822
|
+
});
|
|
2823
|
+
splashDone("runtime", `OK — modules ${mods.join(", ") || "kit"}`);
|
|
2824
|
+
}
|
|
2825
|
+
catch (e) {
|
|
2826
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2827
|
+
logError("h3", e);
|
|
2828
|
+
track({
|
|
2829
|
+
level: "event",
|
|
2830
|
+
kind: "h3.brand_runtime.ready",
|
|
2831
|
+
outcome: "error",
|
|
2832
|
+
reason: reason.slice(0, 300),
|
|
2833
|
+
});
|
|
2834
|
+
splashPatch("runtime", {
|
|
2835
|
+
status: "error",
|
|
2836
|
+
detail: `Échec runtime brand — ${reason.slice(0, 220)}`,
|
|
2837
|
+
percent: 100,
|
|
2838
|
+
headline: "Runtime plateforme en échec (mode dégradé)",
|
|
2839
|
+
});
|
|
2840
|
+
deps.vertical.reportCrash("boot-failure", {
|
|
2841
|
+
step: "brand-runtime",
|
|
2842
|
+
message: reason.slice(0, 500),
|
|
2843
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
/* 3. Meilisearch (recherche) — démarré AVANT le serveur Next */
|
|
2847
|
+
deps.vertical.setBootStage("meili");
|
|
2848
|
+
splashGo("meili", {
|
|
2849
|
+
headline: "Moteur de recherche…",
|
|
2850
|
+
detail: "Démarrage Meilisearch",
|
|
2851
|
+
percent: 20,
|
|
2852
|
+
});
|
|
2853
|
+
const meiliStarted = Date.now();
|
|
2854
|
+
try {
|
|
2855
|
+
meili = await deps.hosts.meili().startMeili(scoped("meili"));
|
|
2856
|
+
}
|
|
2857
|
+
catch (e) {
|
|
2858
|
+
logError("meili", e);
|
|
2859
|
+
meili = null;
|
|
2860
|
+
}
|
|
2861
|
+
trackDecision("meili.start", meili ? "ok" : "unavailable", {
|
|
2862
|
+
durationMs: Date.now() - meiliStarted,
|
|
2863
|
+
ctx: meiliDiskState(),
|
|
2864
|
+
});
|
|
2865
|
+
if (!meili) {
|
|
2866
|
+
splashPatch("meili", {
|
|
2867
|
+
status: "error",
|
|
2868
|
+
detail: "Échec — recherche en mode dégradé",
|
|
2869
|
+
percent: 100,
|
|
2870
|
+
});
|
|
2871
|
+
splashPatch("index", { status: "skip", detail: "Meili indisponible" });
|
|
2872
|
+
deps.vertical.reportCrash("boot-failure", {
|
|
2873
|
+
step: "meili",
|
|
2874
|
+
message: "Meilisearch n'a pas démarré — recherche en mode dégradé explicite",
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
else {
|
|
2878
|
+
splashDone("meili", meili.host);
|
|
2879
|
+
}
|
|
2880
|
+
/* 4. Indexation catalogue (produits / marketplaces) */
|
|
2881
|
+
if (meili) {
|
|
2882
|
+
deps.vertical.setBootStage("index");
|
|
2883
|
+
splashGo("index", {
|
|
2884
|
+
headline: "Indexation du catalogue…",
|
|
2885
|
+
detail: "Contrôle des indexes…",
|
|
2886
|
+
percent: 10,
|
|
2887
|
+
});
|
|
2888
|
+
try {
|
|
2889
|
+
const decision = await meiliIndexReady(meili);
|
|
2890
|
+
if (!decision) {
|
|
2891
|
+
splashPatch("index", {
|
|
2892
|
+
status: "skip",
|
|
2893
|
+
detail: "Contrôle de cohérence indisponible — indexation reportée",
|
|
2894
|
+
percent: 100,
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
else if (!decision.ready) {
|
|
2898
|
+
// Le POURQUOI de la réindexation, visible utilisateur (fini le muet).
|
|
2899
|
+
splashPatch("index", {
|
|
2900
|
+
detail: `Réindexation nécessaire : ${decision.reason}`,
|
|
2901
|
+
percent: 10,
|
|
2902
|
+
});
|
|
2903
|
+
const indexStarted = Date.now();
|
|
2904
|
+
await runIndexerWithProgress(meili);
|
|
2905
|
+
track({
|
|
2906
|
+
level: "event",
|
|
2907
|
+
kind: "index.run",
|
|
2908
|
+
outcome: "ok",
|
|
2909
|
+
reason: decision.reason,
|
|
2910
|
+
durationMs: Date.now() - indexStarted,
|
|
2911
|
+
});
|
|
2912
|
+
splashDone("index", "Index prêt");
|
|
2913
|
+
}
|
|
2914
|
+
else {
|
|
2915
|
+
log("main", "indexation Meili : indexes déjà prêts — skip");
|
|
2916
|
+
splashDone("index", "Indexes déjà prêts");
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
catch (e) {
|
|
2920
|
+
logError("indexer", e);
|
|
2921
|
+
track({
|
|
2922
|
+
level: "error",
|
|
2923
|
+
kind: "index.run",
|
|
2924
|
+
outcome: "error",
|
|
2925
|
+
reason: e instanceof Error ? e.message.slice(0, 300) : String(e),
|
|
2926
|
+
});
|
|
2927
|
+
splashPatch("index", {
|
|
2928
|
+
status: "error",
|
|
2929
|
+
detail: e instanceof Error ? e.message : "Erreur indexation",
|
|
2930
|
+
percent: 100,
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
else {
|
|
2935
|
+
log("main", "indexation Meili : skip (Meili indisponible)");
|
|
2936
|
+
track({
|
|
2937
|
+
level: "decision",
|
|
2938
|
+
kind: "meili.ready",
|
|
2939
|
+
outcome: "skip-no-meili",
|
|
2940
|
+
reason: "meili-indisponible",
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
/*
|
|
2944
|
+
* Stack embeds (contrat produit) :
|
|
2945
|
+
* - install + start UNIQUEMENT ici, pendant le splash ;
|
|
2946
|
+
* - pas d’interface CRM tant que tout n’est pas prêt ;
|
|
2947
|
+
* - l’utilisateur ne télécharge / démarre / arrête rien au clic.
|
|
2948
|
+
*/
|
|
2949
|
+
/* 4a+. Runtime Node marque (pin ≥22.22) — requis dès qu’un outil npm démarre. */
|
|
2950
|
+
if (needNode) {
|
|
2951
|
+
deps.vertical.setBootStage("node-runtime");
|
|
2952
|
+
splashGo("node", {
|
|
2953
|
+
headline: `${nodeLabel}…`,
|
|
2954
|
+
detail: "Vérification / installation du runtime Node piné",
|
|
2955
|
+
percent: 10,
|
|
2956
|
+
});
|
|
2957
|
+
const nodeRt = deps.hosts.nodeRuntime();
|
|
2958
|
+
const ensureNode = nodeRt.ensureDesktopNode;
|
|
2959
|
+
if (!ensureNode) {
|
|
2960
|
+
throw new Error("nodeRuntime.ensureDesktopNode manquant");
|
|
2961
|
+
}
|
|
2962
|
+
const nodeReady = await ensureNode({
|
|
2963
|
+
onLog: (line) => {
|
|
2964
|
+
scoped("node")(line);
|
|
2965
|
+
if (isSplashProgressLine(line)) {
|
|
2966
|
+
const detail = sanitizeSplashDetail(line);
|
|
2967
|
+
splashPatch("node", {
|
|
2968
|
+
detail,
|
|
2969
|
+
percent: estimateEmbedPercent(detail),
|
|
2970
|
+
headline: `${nodeLabel}…`,
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
});
|
|
2975
|
+
if (!nodeReady.ok) {
|
|
2976
|
+
splashPatch("node", {
|
|
2977
|
+
status: "error",
|
|
2978
|
+
detail: nodeReady.detail,
|
|
2979
|
+
percent: 100,
|
|
2980
|
+
});
|
|
2981
|
+
throw new Error(`${nodeLabel} requis pour n8n : ${nodeReady.detail}`);
|
|
2982
|
+
}
|
|
2983
|
+
splashDone("node", `v${nodeReady.version} (${nodeReady.source})\n${nodeReady.node}`);
|
|
2984
|
+
log("main", `${nodeLabel} v${nodeReady.version} (${nodeReady.source}) → ${nodeReady.node}`);
|
|
2985
|
+
}
|
|
2986
|
+
/*
|
|
2987
|
+
* 4b–c. Hermes + n8n EN PARALLÈLE après Node.
|
|
2988
|
+
* Temps splash ≈ max(des 2). Chaque outil a sa propre ligne + barre + chrono.
|
|
2989
|
+
*/
|
|
2990
|
+
deps.vertical.setBootStage("embeds");
|
|
2991
|
+
setSplashStatus("Stack native — démarrage parallèle…");
|
|
2992
|
+
const startEmbed = async (name, label, run) => {
|
|
2993
|
+
splashGo(name, {
|
|
2994
|
+
parallel: true,
|
|
2995
|
+
headline: "Stack native — démarrage parallèle…",
|
|
2996
|
+
detail: `Démarrage ${label}…`,
|
|
2997
|
+
percent: 8,
|
|
2998
|
+
});
|
|
2999
|
+
const embedStarted = Date.now();
|
|
3000
|
+
// Honnêteté splash + flotte : install réelle vs runtime déjà présent
|
|
3001
|
+
// (ex. userData conservé par une désinstallation sans purge).
|
|
3002
|
+
const startPath = () => {
|
|
3003
|
+
try {
|
|
3004
|
+
return name === "hermes"
|
|
3005
|
+
? deps.hosts.hermes().getHermesLastStartPath()
|
|
3006
|
+
: deps.hosts.n8n().getN8nLastStartPath();
|
|
3007
|
+
}
|
|
3008
|
+
catch {
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
try {
|
|
3013
|
+
const r = await run();
|
|
3014
|
+
const p = startPath();
|
|
3015
|
+
track({
|
|
3016
|
+
level: "event",
|
|
3017
|
+
kind: `embed.${name}`,
|
|
3018
|
+
outcome: r ? "ok" : "skipped",
|
|
3019
|
+
durationMs: Date.now() - embedStarted,
|
|
3020
|
+
ctx: { startPath: p },
|
|
3021
|
+
});
|
|
3022
|
+
const readyLabel = name === "hermes" ? "API + WebUI prêts" : "UI prête";
|
|
3023
|
+
const pathLabel = p === "bootstrap"
|
|
3024
|
+
? "installation complète"
|
|
3025
|
+
: p === "reuse"
|
|
3026
|
+
? "runtime existant réutilisé"
|
|
3027
|
+
: null;
|
|
3028
|
+
splashDone(name, r
|
|
3029
|
+
? pathLabel
|
|
3030
|
+
? `OK (${pathLabel}) — ${readyLabel}`
|
|
3031
|
+
: `OK — ${readyLabel}`
|
|
3032
|
+
: "Ignoré");
|
|
3033
|
+
return r;
|
|
3034
|
+
}
|
|
3035
|
+
catch (e) {
|
|
3036
|
+
track({
|
|
3037
|
+
level: "error",
|
|
3038
|
+
kind: `embed.${name}`,
|
|
3039
|
+
outcome: "error",
|
|
3040
|
+
reason: e instanceof Error ? e.message.slice(0, 300) : String(e),
|
|
3041
|
+
durationMs: Date.now() - embedStarted,
|
|
3042
|
+
ctx: { startPath: startPath() },
|
|
3043
|
+
});
|
|
3044
|
+
splashPatch(name, {
|
|
3045
|
+
status: "error",
|
|
3046
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
3047
|
+
percent: 100,
|
|
3048
|
+
});
|
|
3049
|
+
throw e;
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
const [hermesR, n8nR] = await Promise.allSettled([
|
|
3053
|
+
needHermes
|
|
3054
|
+
? startEmbed("hermes", "Hermes", () => deps.hosts.hermes().startHermes({
|
|
3055
|
+
connectionMode: "local",
|
|
3056
|
+
hermesConfig: hermesCfg,
|
|
3057
|
+
onLog: (line) => {
|
|
3058
|
+
scoped("hermes")(line);
|
|
3059
|
+
if (isSplashProgressLine(line))
|
|
3060
|
+
splashEmbedLine("hermes", line);
|
|
3061
|
+
},
|
|
3062
|
+
autoBootstrap: true,
|
|
3063
|
+
}))
|
|
3064
|
+
: Promise.resolve(null),
|
|
3065
|
+
needN8n
|
|
3066
|
+
? startEmbed("n8n", "n8n", () => deps.hosts.n8n().startN8n({
|
|
3067
|
+
connectionMode: "local",
|
|
3068
|
+
n8nConfig: n8nCfg,
|
|
3069
|
+
publicBaseUrl: deps.hosts.tunnel().publicUrlForEmbedService("n8n"),
|
|
3070
|
+
onLog: (line) => {
|
|
3071
|
+
scoped("n8n")(line);
|
|
3072
|
+
if (isSplashProgressLine(line))
|
|
3073
|
+
splashEmbedLine("n8n", line);
|
|
3074
|
+
},
|
|
3075
|
+
autoBootstrap: true,
|
|
3076
|
+
}))
|
|
3077
|
+
: Promise.resolve(null),
|
|
3078
|
+
]);
|
|
3079
|
+
hermes =
|
|
3080
|
+
hermesR.status === "fulfilled"
|
|
3081
|
+
? hermesR.value
|
|
3082
|
+
: null;
|
|
3083
|
+
n8n = n8nR.status === "fulfilled" ? n8nR.value : null;
|
|
3084
|
+
if (hermesR.status === "rejected")
|
|
3085
|
+
logError("hermes", hermesR.reason);
|
|
3086
|
+
if (n8nR.status === "rejected")
|
|
3087
|
+
logError("n8n", n8nR.reason);
|
|
3088
|
+
// Pont Hermes ↔ n8n (clé API) — restart Hermes si 1er provisionnement.
|
|
3089
|
+
if (needHermes && needN8n && hermes && n8n) {
|
|
3090
|
+
try {
|
|
3091
|
+
const bridge = await deps.hosts.hermes().reapplyHermesBridge({
|
|
3092
|
+
connectionMode: "local",
|
|
3093
|
+
onLog: (line) => {
|
|
3094
|
+
scoped("hermes")(line);
|
|
3095
|
+
if (isSplashProgressLine(line))
|
|
3096
|
+
splashEmbedLine("hermes", line);
|
|
3097
|
+
},
|
|
3098
|
+
});
|
|
3099
|
+
if (bridge.restarted)
|
|
3100
|
+
hermes = deps.hosts.hermes().getRunningHermes() ?? hermes;
|
|
3101
|
+
log("main", `bridge n8n→Hermes: ${bridge.detail}`);
|
|
3102
|
+
}
|
|
3103
|
+
catch (e) {
|
|
3104
|
+
logError("hermes-bridge", e);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
if (needHermes && (!hermes?.apiUrl || !hermes.webuiUrl)) {
|
|
3108
|
+
// Soft-boot : TF2 plantait parfois ici (install.sh exit 126 / CLI absente).
|
|
3109
|
+
// On ouvre l’UI quand même + retry Hermes en arrière-plan (pas fail-hard).
|
|
3110
|
+
const st = deps.hosts.hermes().getHermesStatusPayload("local");
|
|
3111
|
+
const hermesSoftMsg = `Hermes indisponible pour l’instant — interface ouverte, retry en arrière-plan.\n${st.detail}${st.bootstrapError ? `\n${st.bootstrapError}` : ""}`;
|
|
3112
|
+
logError("hermes", hermesSoftMsg);
|
|
3113
|
+
splashPatch("hermes", {
|
|
3114
|
+
status: "error",
|
|
3115
|
+
detail: hermesSoftMsg,
|
|
3116
|
+
percent: 100,
|
|
3117
|
+
});
|
|
3118
|
+
track({
|
|
3119
|
+
level: "error",
|
|
3120
|
+
kind: "embed.hermes",
|
|
3121
|
+
outcome: "degraded",
|
|
3122
|
+
reason: hermesSoftMsg.slice(0, 300),
|
|
3123
|
+
});
|
|
3124
|
+
const hermesCfgRetry = hermesCfg;
|
|
3125
|
+
const retryHermesBg = async () => {
|
|
3126
|
+
const delaysMs = [5_000, 15_000, 45_000, 90_000, 180_000];
|
|
3127
|
+
for (let i = 0; i < delaysMs.length; i++) {
|
|
3128
|
+
await new Promise((r) => setTimeout(r, delaysMs[i]));
|
|
3129
|
+
try {
|
|
3130
|
+
if (deps.hosts.hermes().getRunningHermes()?.apiUrl)
|
|
3131
|
+
return;
|
|
3132
|
+
log("hermes", `retry background Hermes (${i + 1}/${delaysMs.length})…`);
|
|
3133
|
+
const again = await deps.hosts.hermes().startHermes({
|
|
3134
|
+
connectionMode: "local",
|
|
3135
|
+
hermesConfig: hermesCfgRetry,
|
|
3136
|
+
onLog: (line) => scoped("hermes")(line),
|
|
3137
|
+
autoBootstrap: true,
|
|
3138
|
+
});
|
|
3139
|
+
if (again?.apiUrl) {
|
|
3140
|
+
hermes = again;
|
|
3141
|
+
log("main", `Hermes API prêt (retry) sur ${again.apiUrl}${again.webuiUrl ? ` · WebUI ${again.webuiUrl}` : ""}`);
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
catch (e) {
|
|
3146
|
+
logError("hermes-retry", e);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
log("hermes", "retry background épuisé — Hermes reste indisponible");
|
|
3150
|
+
};
|
|
3151
|
+
void retryHermesBg();
|
|
3152
|
+
}
|
|
3153
|
+
if (needN8n && !n8n?.uiUrl) {
|
|
3154
|
+
const st = deps.hosts.n8n().getN8nStatusPayload("local");
|
|
3155
|
+
const extra = st.bootstrapError && st.bootstrapError !== st.detail
|
|
3156
|
+
? `\n${st.bootstrapError}`
|
|
3157
|
+
: "";
|
|
3158
|
+
throw new Error(`n8n doit être prêt avant l’interface (install + démarrage au splash uniquement).\n${st.detail}${extra}`);
|
|
3159
|
+
}
|
|
3160
|
+
if (hermes) {
|
|
3161
|
+
splashDone("hermes", `API ${hermes.apiUrl}\nWebUI ${hermes.webuiUrl || "—"}`);
|
|
3162
|
+
log("main", `Hermes API prêt sur ${hermes.apiUrl}${hermes.webuiUrl ? ` · WebUI ${hermes.webuiUrl}` : ""}`);
|
|
3163
|
+
}
|
|
3164
|
+
else {
|
|
3165
|
+
log("main", `Hermes skip (mode=${hermesCfg.mode})`);
|
|
3166
|
+
}
|
|
3167
|
+
if (n8n) {
|
|
3168
|
+
splashDone("n8n", `UI ${n8n.uiUrl}`);
|
|
3169
|
+
log("main", `n8n UI prêt sur ${n8n.uiUrl}`);
|
|
3170
|
+
}
|
|
3171
|
+
else
|
|
3172
|
+
log("main", `n8n skip (mode=${n8nCfg.mode})`);
|
|
3173
|
+
setSplashStatus("Stack native prête — finalisation…");
|
|
3174
|
+
/* 5. Serveur Next */
|
|
3175
|
+
deps.vertical.setBootStage("next-server");
|
|
3176
|
+
splashGo("next", {
|
|
3177
|
+
headline: "Démarrage du serveur local…",
|
|
3178
|
+
detail: "Next.js CRM",
|
|
3179
|
+
percent: 25,
|
|
3180
|
+
});
|
|
3181
|
+
const publicUrl = deps.hosts.tunnel().publicUrlForServer();
|
|
3182
|
+
const bindHost = currentBindHost();
|
|
3183
|
+
server = await deps.hosts.server().startNextServer({
|
|
3184
|
+
meiliHost: meili?.host ?? null,
|
|
3185
|
+
meiliMasterKey: meili?.masterKey ?? null,
|
|
3186
|
+
bindHost,
|
|
3187
|
+
extraEnv: {
|
|
3188
|
+
...(publicUrl
|
|
3189
|
+
? { APP_PUBLIC_URL: publicUrl, MCP_PUBLIC_URL: publicUrl }
|
|
3190
|
+
: {}),
|
|
3191
|
+
...deps.hosts.hermes().getHermesNextEnv("local"),
|
|
3192
|
+
...deps.hosts.n8n().getN8nNextEnv("local"),
|
|
3193
|
+
...deps.store().getEmailNextEnv(),
|
|
3194
|
+
// Onglets Données / n8n du Product Hub côté Next (pluginDataPath).
|
|
3195
|
+
[pluginsDirEnvKey]: deps.hosts.pluginRuntime().pluginsRootDir(),
|
|
3196
|
+
},
|
|
3197
|
+
onLog: scoped("next"),
|
|
3198
|
+
});
|
|
3199
|
+
attachNextExitHandler(server);
|
|
3200
|
+
activeCrmBaseUrl = server.baseUrl;
|
|
3201
|
+
syncServerLlmActiveFromStored();
|
|
3202
|
+
splashDone("next", server.baseUrl);
|
|
3203
|
+
log("main", `serveur Next prêt sur ${server.baseUrl} (bind ${bindHost}, BYOK openai=${serverLlmActive.openai} anthropic=${serverLlmActive.anthropic})`);
|
|
3204
|
+
/* 5a. Clé CRM Hermes + plugins control plane, puis seed context/MCP + 1 restart */
|
|
3205
|
+
let hermesCrmApiKey = null;
|
|
3206
|
+
if (needHermes && hermes) {
|
|
3207
|
+
try {
|
|
3208
|
+
const crmKey = await deps.hosts.hermesCrmKey().ensureHermesCrmApiKey({
|
|
3209
|
+
log: (line) => scoped("hermes")(line),
|
|
3210
|
+
});
|
|
3211
|
+
log("main", `crm-key Hermes: ${crmKey.detail}`);
|
|
3212
|
+
hermesCrmApiKey = crmKey.apiKey;
|
|
3213
|
+
}
|
|
3214
|
+
catch (e) {
|
|
3215
|
+
logError("hermes-crm-bridge", e);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
/* 5a1b. D1 — façade MCP → proxy Hono dès que Next écoute */
|
|
3219
|
+
try {
|
|
3220
|
+
const brandRt = deps.vertical.getActiveBrandRuntime();
|
|
3221
|
+
if (brandRt) {
|
|
3222
|
+
brandRt.setMcpUpstream(server.baseUrl, hermesCrmApiKey || process.env[apiKeyEnvName] || null);
|
|
3223
|
+
log("mcp", `D1 upstream Hono → ${server.baseUrl}/mcp ` +
|
|
3224
|
+
`(executor=${brandRt.mcpProductExecutor})`);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
catch (e) {
|
|
3228
|
+
logError("mcp-upstream", e);
|
|
3229
|
+
}
|
|
3230
|
+
/* 5a2. Plugins user (sidecars) + control plane */
|
|
3231
|
+
try {
|
|
3232
|
+
deps.hosts.plugins().setPluginsCrmPort(server.port);
|
|
3233
|
+
const plug = await deps.hosts.plugins().startEnabledPlugins({
|
|
3234
|
+
onLog: (line) => scoped("plugins")(line),
|
|
3235
|
+
});
|
|
3236
|
+
if (plug.started.length) {
|
|
3237
|
+
log("main", `plugins démarrés: ${plug.started.join(", ")}`);
|
|
3238
|
+
}
|
|
3239
|
+
if (plug.errors.length) {
|
|
3240
|
+
log("main", `plugins erreurs: ${plug.errors.join(" | ")}`);
|
|
3241
|
+
}
|
|
3242
|
+
const plugApi = await deps.hosts.pluginControl().startPluginControlApi({
|
|
3243
|
+
log: (line) => scoped("plugins")(line),
|
|
3244
|
+
});
|
|
3245
|
+
log("main", `plugins-api: ${plugApi.url}`);
|
|
3246
|
+
}
|
|
3247
|
+
catch (e) {
|
|
3248
|
+
logError("plugins", e);
|
|
3249
|
+
}
|
|
3250
|
+
/* 5a3. Context pack (prefs, OpenAPI, glossaire) + MCP tempoflow + bridge Hermes */
|
|
3251
|
+
if (needHermes && hermes) {
|
|
3252
|
+
try {
|
|
3253
|
+
if (hermesCrmApiKey) {
|
|
3254
|
+
const ctx = await deps.hosts.hermesSeed().seedHermesContext({
|
|
3255
|
+
crmPort: server.port,
|
|
3256
|
+
apiKey: hermesCrmApiKey,
|
|
3257
|
+
log: (line) => scoped("hermes")(line),
|
|
3258
|
+
});
|
|
3259
|
+
log("main", `hermes-context: ${ctx.files.length} fichiers → ${ctx.dir} (mcp ${ctx.mcpUrl})`);
|
|
3260
|
+
}
|
|
3261
|
+
const bridge = await deps.hosts.hermes().reapplyHermesBridge({
|
|
3262
|
+
connectionMode: "local",
|
|
3263
|
+
crmPort: server.port,
|
|
3264
|
+
forceRestart: Boolean(hermesCrmApiKey),
|
|
3265
|
+
forceReason: "context+mcp+plugins",
|
|
3266
|
+
onLog: (line) => scoped("hermes")(line),
|
|
3267
|
+
});
|
|
3268
|
+
if (bridge.restarted) {
|
|
3269
|
+
hermes = deps.hosts.hermes().getRunningHermes() ?? hermes;
|
|
3270
|
+
// Hermes a pu prendre un nouveau port — Next doit recevoir HERMES_API_URL.
|
|
3271
|
+
await maybeRestartNextAfterHermesSpawn(true);
|
|
3272
|
+
}
|
|
3273
|
+
else if (!deps.hosts.hermes().getRunningHermes()?.webuiUrl) {
|
|
3274
|
+
throw new Error(bridge.detail);
|
|
3275
|
+
}
|
|
3276
|
+
log("main", `bridge CRM→Hermes: ${bridge.detail}`);
|
|
3277
|
+
}
|
|
3278
|
+
catch (e) {
|
|
3279
|
+
logError("hermes-context-bridge", e);
|
|
3280
|
+
const status = deps.hosts.hermes().getHermesStatusPayload("local");
|
|
3281
|
+
deps.vertical.reportCrashDebounced("child-exit", `hermes-context-bridge:${status.detail}`, {
|
|
3282
|
+
service: "hermes",
|
|
3283
|
+
action: "context-bridge-restart",
|
|
3284
|
+
status: status.status,
|
|
3285
|
+
webuiStatus: status.webuiStatus,
|
|
3286
|
+
detail: status.detail,
|
|
3287
|
+
bootstrapError: status.bootstrapError,
|
|
3288
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3289
|
+
logs: deps.hosts.hermes().getHermesLogs().slice(-80),
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
/* 5b. Tunnel Cloudflare (si réservé) — accès distant (desktop / browser / mobile) */
|
|
3294
|
+
if (publicUrl) {
|
|
3295
|
+
deps.vertical.setBootStage("tunnel");
|
|
3296
|
+
splashGo("tunnel", {
|
|
3297
|
+
headline: "Tunnel d’accès distant…",
|
|
3298
|
+
detail: publicUrl,
|
|
3299
|
+
percent: 30,
|
|
3300
|
+
});
|
|
3301
|
+
try {
|
|
3302
|
+
await syncTunnelIngress();
|
|
3303
|
+
await deps.hosts.tunnel().startCloudflared();
|
|
3304
|
+
// n8n a démarré avant le tunnel → réinjecter WEBHOOK_URL publique.
|
|
3305
|
+
if (needN8n) {
|
|
3306
|
+
splashPatch("n8n", {
|
|
3307
|
+
status: "running",
|
|
3308
|
+
detail: "Alignement webhooks sur le tunnel…",
|
|
3309
|
+
percent: 92,
|
|
3310
|
+
headline: "Tunnel d’accès distant…",
|
|
3311
|
+
});
|
|
3312
|
+
await syncN8nWebhookPublicUrl((line) => {
|
|
3313
|
+
scoped("n8n")(line);
|
|
3314
|
+
if (isSplashProgressLine(line))
|
|
3315
|
+
splashEmbedLine("n8n", line);
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
const st = deps.hosts.tunnel().getTunnelStatus();
|
|
3319
|
+
const urls = st.publicUrls;
|
|
3320
|
+
trackDecision("tunnel.connect", st.online ? "online" : "configured", {
|
|
3321
|
+
ctx: { slug: deps.store().getTunnelConfig()?.slug },
|
|
3322
|
+
});
|
|
3323
|
+
splashDone("tunnel", urls
|
|
3324
|
+
? `CRM ${publicUrl}\nn8n ${urls.n8n}\nHermes ${urls.hermes}`
|
|
3325
|
+
: publicUrl);
|
|
3326
|
+
log("main", `tunnel d'accès distant prêt → CRM ${publicUrl}` +
|
|
3327
|
+
(urls ? ` · n8n ${urls.n8n} · hermes ${urls.hermes}` : ""));
|
|
3328
|
+
}
|
|
3329
|
+
catch (e) {
|
|
3330
|
+
logError("tunnel", e);
|
|
3331
|
+
trackDecision("tunnel.connect", "error", {
|
|
3332
|
+
reason: e instanceof Error ? e.message.slice(0, 300) : String(e),
|
|
3333
|
+
});
|
|
3334
|
+
splashPatch("tunnel", {
|
|
3335
|
+
status: "error",
|
|
3336
|
+
detail: (e instanceof Error ? e.message : String(e)) +
|
|
3337
|
+
"\n(CRM local reste utilisable)",
|
|
3338
|
+
percent: 100,
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
/* 6. First-run setup OU login conditionnel + UI */
|
|
3343
|
+
deps.vertical.setBootStage("load-ui");
|
|
3344
|
+
routeExternalLinksToTabs(view, server.baseUrl);
|
|
3345
|
+
if (deps.appKind === "server") {
|
|
3346
|
+
installServerCockpitGuard(view, server.baseUrl);
|
|
3347
|
+
}
|
|
3348
|
+
if (!deps.store().isSetupComplete()) {
|
|
3349
|
+
splashGo("login", {
|
|
3350
|
+
headline: "Configuration initiale…",
|
|
3351
|
+
detail: "Ouverture de /setup",
|
|
3352
|
+
percent: 90,
|
|
3353
|
+
});
|
|
3354
|
+
log("main", "setup incomplet → /setup");
|
|
3355
|
+
await view.webContents.loadURL(`${server.baseUrl}/setup`);
|
|
3356
|
+
splashDone("login", "Écran de configuration");
|
|
3357
|
+
}
|
|
3358
|
+
else {
|
|
3359
|
+
if (deps.store().shouldAutoLoginOnBoot()) {
|
|
3360
|
+
deps.vertical.setBootStage("auto-login");
|
|
3361
|
+
splashGo("login", {
|
|
3362
|
+
headline: "Connexion…",
|
|
3363
|
+
detail: "Session locale",
|
|
3364
|
+
percent: 80,
|
|
3365
|
+
});
|
|
3366
|
+
try {
|
|
3367
|
+
await autoLogin(server.baseUrl);
|
|
3368
|
+
}
|
|
3369
|
+
catch (e) {
|
|
3370
|
+
logError("auto-login", e);
|
|
3371
|
+
}
|
|
3372
|
+
// Q5 (multi-profils) : en mode serveur (« Héberger »), la fenêtre
|
|
3373
|
+
// principale ouvre le COCKPIT ; app Serveur (kind=server) → cockpit
|
|
3374
|
+
// AUTONOME /server-cockpit (sans sidebar CRM) ; legacy → /cockpit.
|
|
3375
|
+
await view.webContents.loadURL(`${server.baseUrl}${deps.bootBehavior.cockpitPath}`);
|
|
3376
|
+
splashDone("login", "Cockpit ouvert");
|
|
3377
|
+
}
|
|
3378
|
+
else {
|
|
3379
|
+
splashGo("login", {
|
|
3380
|
+
headline: "Prêt",
|
|
3381
|
+
detail: "Ouverture de /login",
|
|
3382
|
+
percent: 90,
|
|
3383
|
+
});
|
|
3384
|
+
log("main", "pas d'auto-login (rester connecté désactivé ou déconnexion) → /login");
|
|
3385
|
+
// Après login manuel, atterrir sur le cockpit (Q5) via ?next=.
|
|
3386
|
+
await view.webContents.loadURL(`${server.baseUrl}/login?next=${encodeURIComponent(deps.bootBehavior.cockpitPath)}`);
|
|
3387
|
+
splashDone("login", "Écran de connexion");
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
// La fenêtre CRM est maintenant visible. Les embeds peuvent télécharger
|
|
3391
|
+
// Node/n8n/Hermes sans écran noir et sans dialogue bloquant. Les statuts
|
|
3392
|
+
// restent consultables dans le shell, les logs et le cockpit.
|
|
3393
|
+
if (deferEmbedWarm && (configuredNeedHermes || configuredNeedN8n)) {
|
|
3394
|
+
void (async () => {
|
|
3395
|
+
log("native", "warm différé après UI — Hermes/n8n s'installent en arrière-plan");
|
|
3396
|
+
if (configuredNeedN8n) {
|
|
3397
|
+
const nodeRuntime = deps.hosts.nodeRuntime();
|
|
3398
|
+
try {
|
|
3399
|
+
const node = await nodeRuntime.ensureDesktopNode?.({
|
|
3400
|
+
onLog: (line) => log("node", line),
|
|
3401
|
+
});
|
|
3402
|
+
if (!node?.ok) {
|
|
3403
|
+
log("native", `warm n8n différé ignoré: ${node?.detail || "Node indisponible"}`);
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
catch (e) {
|
|
3408
|
+
logError("node", e);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
const [hermesWarm, n8nWarm] = await Promise.allSettled([
|
|
3413
|
+
configuredNeedHermes
|
|
3414
|
+
? deps.hosts.hermes().startHermes({
|
|
3415
|
+
connectionMode: "local",
|
|
3416
|
+
hermesConfig: hermesCfg,
|
|
3417
|
+
autoBootstrap: true,
|
|
3418
|
+
onLog: (line) => log("hermes", line),
|
|
3419
|
+
})
|
|
3420
|
+
: Promise.resolve(null),
|
|
3421
|
+
configuredNeedN8n
|
|
3422
|
+
? deps.hosts.n8n().startN8n({
|
|
3423
|
+
connectionMode: "local",
|
|
3424
|
+
n8nConfig: n8nCfg,
|
|
3425
|
+
autoBootstrap: true,
|
|
3426
|
+
onLog: (line) => log("n8n", line),
|
|
3427
|
+
})
|
|
3428
|
+
: Promise.resolve(null),
|
|
3429
|
+
]);
|
|
3430
|
+
if (hermesWarm.status === "fulfilled" && hermesWarm.value) {
|
|
3431
|
+
hermes = hermesWarm.value;
|
|
3432
|
+
log("native", "warm Hermes différé prêt");
|
|
3433
|
+
}
|
|
3434
|
+
else if (hermesWarm.status === "rejected") {
|
|
3435
|
+
logError("hermes", hermesWarm.reason);
|
|
3436
|
+
}
|
|
3437
|
+
if (n8nWarm.status === "fulfilled" && n8nWarm.value) {
|
|
3438
|
+
n8n = n8nWarm.value;
|
|
3439
|
+
log("native", "warm n8n différé prêt");
|
|
3440
|
+
}
|
|
3441
|
+
else if (n8nWarm.status === "rejected") {
|
|
3442
|
+
logError("n8n", n8nWarm.reason);
|
|
3443
|
+
}
|
|
3444
|
+
})();
|
|
3445
|
+
}
|
|
3446
|
+
/* 7. Bridge bot ↔ onglets fournisseurs (après compte créé) */
|
|
3447
|
+
deps.vertical.setBootStage("bridge");
|
|
3448
|
+
await startBridgeIfReady();
|
|
3449
|
+
/* 8. Agent flotte (opt-in Config → Support) */
|
|
3450
|
+
deps.hosts.fleetAgent().startFleetAgent({
|
|
3451
|
+
appKind: deps.appKind,
|
|
3452
|
+
getHealth: () => {
|
|
3453
|
+
const hermes = deps.hosts.hermes().getHermesStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
3454
|
+
const n8nSt = deps.hosts.n8n().getN8nStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
3455
|
+
const tun = deps.hosts.tunnel().getTunnelStatus();
|
|
3456
|
+
return {
|
|
3457
|
+
next: server ? "running" : "stopped",
|
|
3458
|
+
meili: meili ? "running" : "stopped",
|
|
3459
|
+
hermes: hermes.status,
|
|
3460
|
+
n8n: n8nSt.status,
|
|
3461
|
+
tunnel: tun.online ? "running" : tun.configured ? "configured" : "off",
|
|
3462
|
+
};
|
|
3463
|
+
},
|
|
3464
|
+
getHeartbeatExtras: () => {
|
|
3465
|
+
if (typeof deps.vertical.getHeartbeatExtras === "function") {
|
|
3466
|
+
try {
|
|
3467
|
+
return deps.vertical.getHeartbeatExtras();
|
|
3468
|
+
}
|
|
3469
|
+
catch {
|
|
3470
|
+
return null;
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
return null;
|
|
3474
|
+
},
|
|
3475
|
+
getPluginsSummary: () => {
|
|
3476
|
+
try {
|
|
3477
|
+
const st = deps.hosts.plugins().pluginsStatusPayload();
|
|
3478
|
+
return (st.plugins || []).map((p) => ({
|
|
3479
|
+
id: p.manifest.id,
|
|
3480
|
+
name: p.manifest.name,
|
|
3481
|
+
version: p.manifest.version,
|
|
3482
|
+
enabled: p.enabled,
|
|
3483
|
+
}));
|
|
3484
|
+
}
|
|
3485
|
+
catch {
|
|
3486
|
+
return [];
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3489
|
+
getAssistantChatsSample: () => deps.hosts.fleetSamples().sampleAssistantChats(40),
|
|
3490
|
+
getHermesChatsSample: () => deps.hosts.fleetSamples().sampleHermesChats(20),
|
|
3491
|
+
getRequestLogsSample: () => deps.hosts.fleetSamples().sampleRequestLogs(40),
|
|
3492
|
+
getUsersSummary: () => deps.hosts.fleetSamples().sampleUsers(),
|
|
3493
|
+
getSessionsSummary: () => {
|
|
3494
|
+
const live = deps.hosts.fleetSamples().sampleSessions();
|
|
3495
|
+
if (live.length)
|
|
3496
|
+
return live;
|
|
3497
|
+
return deps.hosts.fleetSamples().sampleUsers()
|
|
3498
|
+
.filter((u) => u.active)
|
|
3499
|
+
.map((u) => ({
|
|
3500
|
+
userId: u.id,
|
|
3501
|
+
username: u.username,
|
|
3502
|
+
lastSeen: new Date().toISOString(),
|
|
3503
|
+
}));
|
|
3504
|
+
},
|
|
3505
|
+
getHermesStats: () => {
|
|
3506
|
+
try {
|
|
3507
|
+
const st = deps.hosts.hermes().getHermesStatusPayload(activeConnectionProfile.mode, remoteStatusOpts());
|
|
3508
|
+
return {
|
|
3509
|
+
status: st.status,
|
|
3510
|
+
webuiStatus: st.webuiStatus,
|
|
3511
|
+
version: st.version,
|
|
3512
|
+
bootstrapPhase: st.bootstrapPhase,
|
|
3513
|
+
installing: st.installing,
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
catch {
|
|
3517
|
+
return {};
|
|
3518
|
+
}
|
|
3519
|
+
},
|
|
3520
|
+
getActionsSample: () => {
|
|
3521
|
+
const ctx = getFleetSessionContext();
|
|
3522
|
+
let userId = ctx.userId;
|
|
3523
|
+
let username = ctx.username;
|
|
3524
|
+
if (!userId) {
|
|
3525
|
+
const users = deps.hosts.fleetSamples().sampleUsers().filter((u) => u.kind !== "ai" && u.active !== false);
|
|
3526
|
+
const owner = users.find((u) => u.role === "owner") || users[0] || null;
|
|
3527
|
+
if (owner) {
|
|
3528
|
+
userId = owner.id;
|
|
3529
|
+
username = owner.username;
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
return sampleFleetActions(150).map((a) => ({
|
|
3533
|
+
...a,
|
|
3534
|
+
userId: a.userId || userId || undefined,
|
|
3535
|
+
username: a.username || username || undefined,
|
|
3536
|
+
sessionId: a.sessionId || ctx.sessionId || undefined,
|
|
3537
|
+
}));
|
|
3538
|
+
},
|
|
3539
|
+
executeRemoteCommand: async (cmd) => {
|
|
3540
|
+
switch (cmd) {
|
|
3541
|
+
case "force-update-check": {
|
|
3542
|
+
const s = await checkForUpdatesNow();
|
|
3543
|
+
return { ok: true, detail: `update state=${s.state}` };
|
|
3544
|
+
}
|
|
3545
|
+
case "sync-now": {
|
|
3546
|
+
const ok = await deps.hosts.fleetAgent().sendFleetHeartbeat();
|
|
3547
|
+
return { ok, detail: ok ? "heartbeat sent" : "heartbeat skipped/failed" };
|
|
3548
|
+
}
|
|
3549
|
+
case "upload-diagnostics": {
|
|
3550
|
+
return deps.hosts.fleetAgent().uploadFleetDiagnostics("remote-command");
|
|
3551
|
+
}
|
|
3552
|
+
case "restart-n8n": {
|
|
3553
|
+
if (activeConnectionProfile.mode !== "local") {
|
|
3554
|
+
return { ok: false, detail: "client distant" };
|
|
3555
|
+
}
|
|
3556
|
+
deps.hosts.n8n().stopN8n();
|
|
3557
|
+
const started = await deps.hosts.n8n().startN8n({
|
|
3558
|
+
connectionMode: "local",
|
|
3559
|
+
n8nConfig: deps.store().getN8nEmbedConfig(),
|
|
3560
|
+
autoBootstrap: false,
|
|
3561
|
+
onLog: (line) => log("n8n", line),
|
|
3562
|
+
});
|
|
3563
|
+
n8n = started;
|
|
3564
|
+
return {
|
|
3565
|
+
ok: Boolean(started),
|
|
3566
|
+
detail: started ? `n8n ${started.uiUrl}` : "n8n start failed",
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
case "restart-hermes": {
|
|
3570
|
+
if (activeConnectionProfile.mode !== "local") {
|
|
3571
|
+
return { ok: false, detail: "client distant" };
|
|
3572
|
+
}
|
|
3573
|
+
await deps.hosts.hermes().stopHermesAndWait();
|
|
3574
|
+
const started = await deps.hosts.hermes().startHermes({
|
|
3575
|
+
connectionMode: "local",
|
|
3576
|
+
hermesConfig: deps.store().getHermesEmbedConfig(),
|
|
3577
|
+
onLog: (line) => log("hermes", line),
|
|
3578
|
+
});
|
|
3579
|
+
hermes = started;
|
|
3580
|
+
await maybeRestartNextAfterHermesSpawn(Boolean(started));
|
|
3581
|
+
return {
|
|
3582
|
+
ok: Boolean(started),
|
|
3583
|
+
detail: started ? `hermes ${started.apiUrl}` : "hermes start failed",
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
default:
|
|
3587
|
+
return { ok: false, detail: `commande refusée: ${cmd}` };
|
|
3588
|
+
}
|
|
3589
|
+
},
|
|
3590
|
+
});
|
|
3591
|
+
deps.vertical.setBootStage("ready");
|
|
3592
|
+
setUpdaterRenderer(sendUpdateToWebContents(view.webContents));
|
|
3593
|
+
log("main", "démarrage terminé.");
|
|
3594
|
+
// Boîte noire : boot terminé + durées par étape (timeline crash-reporter),
|
|
3595
|
+
// résumé persisté (index.json) puis règles d'anomalies sur l'historique.
|
|
3596
|
+
try {
|
|
3597
|
+
const timeline = deps.vertical.getBootTimeline();
|
|
3598
|
+
const stageDurations = {};
|
|
3599
|
+
for (let i = 0; i < timeline.length - 1; i++) {
|
|
3600
|
+
const cur = timeline[i];
|
|
3601
|
+
const next = timeline[i + 1];
|
|
3602
|
+
stageDurations[cur.stage] =
|
|
3603
|
+
(stageDurations[cur.stage] || 0) +
|
|
3604
|
+
Math.max(0, new Date(next.at).getTime() - new Date(cur.at).getTime());
|
|
3605
|
+
}
|
|
3606
|
+
const bootDuration = bootLocalStartedAt ? Date.now() - bootLocalStartedAt : undefined;
|
|
3607
|
+
track({
|
|
3608
|
+
level: "event",
|
|
3609
|
+
kind: "boot.done",
|
|
3610
|
+
outcome: "ok",
|
|
3611
|
+
durationMs: bootDuration,
|
|
3612
|
+
ctx: { stages: stageDurations },
|
|
3613
|
+
});
|
|
3614
|
+
persistBootSummary({
|
|
3615
|
+
endedAt: new Date().toISOString(),
|
|
3616
|
+
durationMs: bootDuration,
|
|
3617
|
+
});
|
|
3618
|
+
evaluateBootRules();
|
|
3619
|
+
}
|
|
3620
|
+
catch (e) {
|
|
3621
|
+
logError("ops", e);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function waitForRetry() {
|
|
3625
|
+
return new Promise((resolve) => ipcMain.once("setup:retry", () => resolve()));
|
|
3626
|
+
}
|
|
3627
|
+
/** Snapshot embeds + stages pour diagnostiquer un splash KO à distance. */
|
|
3628
|
+
function bootFailureContext(extra = {}) {
|
|
3629
|
+
try {
|
|
3630
|
+
return {
|
|
3631
|
+
...extra,
|
|
3632
|
+
hermes: (() => {
|
|
3633
|
+
try {
|
|
3634
|
+
const s = deps.hosts.hermes().getHermesStatusPayload("local");
|
|
3635
|
+
return {
|
|
3636
|
+
status: s.status,
|
|
3637
|
+
detail: s.detail,
|
|
3638
|
+
bootstrapPhase: s.bootstrapPhase,
|
|
3639
|
+
bootstrapError: s.bootstrapError,
|
|
3640
|
+
logs: deps.hosts.hermes().getHermesLogs().slice(-60),
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
catch (e) {
|
|
3644
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
3645
|
+
}
|
|
3646
|
+
})(),
|
|
3647
|
+
n8n: (() => {
|
|
3648
|
+
try {
|
|
3649
|
+
const s = deps.hosts.n8n().getN8nStatusPayload("local");
|
|
3650
|
+
return {
|
|
3651
|
+
status: s.status,
|
|
3652
|
+
detail: s.detail,
|
|
3653
|
+
bootstrapPhase: s.bootstrapPhase,
|
|
3654
|
+
bootstrapError: s.bootstrapError,
|
|
3655
|
+
logs: deps.hosts.n8n().getN8nLogs().slice(-60),
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
catch (e) {
|
|
3659
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
3660
|
+
}
|
|
3661
|
+
})(),
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
catch {
|
|
3665
|
+
return { ...extra };
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
async function bootWithRetry() {
|
|
3669
|
+
const view = await createShell();
|
|
3670
|
+
// Tray dès la coquille créée : fermer la fenêtre pendant le boot ne tue pas
|
|
3671
|
+
// le serveur. Dégradation gracieuse : sans tray, close = quit (historique).
|
|
3672
|
+
trayController = new TrayController({
|
|
3673
|
+
productName: deps.manifest.client.productName,
|
|
3674
|
+
resourcesRoot: deps.paths.resourcesRoot(),
|
|
3675
|
+
isPackaged: deps.paths.isPackaged(),
|
|
3676
|
+
showWindow: showMainWindow,
|
|
3677
|
+
quit: () => {
|
|
3678
|
+
quitting = true;
|
|
3679
|
+
app.quit();
|
|
3680
|
+
},
|
|
3681
|
+
// Sous-menu « Workspaces IA » : fenêtres profils IA (P2, Q8).
|
|
3682
|
+
listAiWorkspaces: () => aiWorkspaces?.listWindows() ?? [],
|
|
3683
|
+
openAiWorkspace: (userId) => {
|
|
3684
|
+
aiWorkspaces?.openWindow(userId);
|
|
3685
|
+
trayController?.refresh();
|
|
3686
|
+
},
|
|
3687
|
+
closeAiWorkspace: (userId) => {
|
|
3688
|
+
aiWorkspaces?.closeWorkspace(userId);
|
|
3689
|
+
trayController?.refresh();
|
|
3690
|
+
},
|
|
3691
|
+
});
|
|
3692
|
+
trayController.setup();
|
|
3693
|
+
// Prefs NSIS (démarrage auto) → sync config locale une seule fois.
|
|
3694
|
+
if (app.isPackaged) {
|
|
3695
|
+
try {
|
|
3696
|
+
const installerPrefs = deps.vertical.consumeInstallerPrefsFile(deps.paths.userDataDir());
|
|
3697
|
+
if (typeof installerPrefs?.launchAtStartup === "boolean") {
|
|
3698
|
+
deps.store().setBackgroundSettings({
|
|
3699
|
+
launchAtStartup: installerPrefs.launchAtStartup,
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
catch (e) {
|
|
3704
|
+
logError("installer-prefs", e);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
// Démarrage auto : réappliqué à chaque boot (l'OS reste la source de vérité,
|
|
3708
|
+
// setLoginItemSettings est idempotent). Packagé uniquement — en dev on ne
|
|
3709
|
+
// veut pas enregistrer le binaire electron du repo.
|
|
3710
|
+
if (app.isPackaged) {
|
|
3711
|
+
applyLaunchAtStartup(deps.store().getBackgroundSettings().launchAtStartup);
|
|
3712
|
+
}
|
|
3713
|
+
// Check update dès l’entrée (splash / picker) — pas après le boot Next.
|
|
3714
|
+
const updaterLog = scoped("updater");
|
|
3715
|
+
void setupAutoUpdater({
|
|
3716
|
+
log: updaterLog,
|
|
3717
|
+
send: sendUpdateToWebContents(view.webContents),
|
|
3718
|
+
feedUrl: deps.appKind === "server"
|
|
3719
|
+
? deps.manifest.server.feedUrl
|
|
3720
|
+
: deps.manifest.client.feedUrl,
|
|
3721
|
+
onTrack: (event) => {
|
|
3722
|
+
if (event.kind === "updater.downloaded") {
|
|
3723
|
+
track({
|
|
3724
|
+
level: "event",
|
|
3725
|
+
kind: "updater.downloaded",
|
|
3726
|
+
ctx: event.ctx,
|
|
3727
|
+
});
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
if (event.outcome === "error") {
|
|
3731
|
+
track({
|
|
3732
|
+
level: "error",
|
|
3733
|
+
kind: "updater.check",
|
|
3734
|
+
outcome: "error",
|
|
3735
|
+
reason: event.reason,
|
|
3736
|
+
});
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
track({
|
|
3740
|
+
level: "decision",
|
|
3741
|
+
kind: "updater.check",
|
|
3742
|
+
outcome: event.outcome,
|
|
3743
|
+
ctx: event.ctx,
|
|
3744
|
+
});
|
|
3745
|
+
},
|
|
3746
|
+
});
|
|
3747
|
+
for (;;) {
|
|
3748
|
+
try {
|
|
3749
|
+
await ensureConnectionChosen(view);
|
|
3750
|
+
await setupAndStart(view);
|
|
3751
|
+
bootReady = true;
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
catch (e) {
|
|
3755
|
+
bootReady = false;
|
|
3756
|
+
logError("main", e);
|
|
3757
|
+
track({
|
|
3758
|
+
level: "error",
|
|
3759
|
+
kind: "boot.failed",
|
|
3760
|
+
reason: e instanceof Error ? e.message.slice(0, 300) : String(e),
|
|
3761
|
+
durationMs: bootLocalStartedAt ? Date.now() - bootLocalStartedAt : undefined,
|
|
3762
|
+
});
|
|
3763
|
+
persistBootSummary({ endedAt: new Date().toISOString() });
|
|
3764
|
+
deps.vertical.reportCrash("boot-failure", bootFailureContext({
|
|
3765
|
+
step: "boot",
|
|
3766
|
+
message: e instanceof Error ? e.message : String(e),
|
|
3767
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
3768
|
+
}));
|
|
3769
|
+
if (view.webContents.isDestroyed())
|
|
3770
|
+
return;
|
|
3771
|
+
await view.webContents
|
|
3772
|
+
.loadURL(errorHtml("Le démarrage a échoué", e instanceof Error ? e.message : String(e)))
|
|
3773
|
+
.catch(() => { });
|
|
3774
|
+
await waitForRetry();
|
|
3775
|
+
// Réafficher le picker si le profil distant est HS (ou laisser l'utilisateur changer).
|
|
3776
|
+
connectionChoiceResolver = null;
|
|
3777
|
+
await view.webContents.loadURL(splashHtmlUrl()).catch(() => { });
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
app.whenReady().then(() => {
|
|
3782
|
+
// Pas de menu File/Edit/View (chrome Notion-like).
|
|
3783
|
+
Menu.setApplicationMenu(null);
|
|
3784
|
+
// Logger + crash reporting AVANT toute étape susceptible d'échouer.
|
|
3785
|
+
initLogger(deps.paths.userDataDir(), deps.manifest.logBasename);
|
|
3786
|
+
deps.vertical.initCrashReporter(deps.paths.userDataDir(), app.getVersion());
|
|
3787
|
+
deps.vertical.installGlobalHandlers();
|
|
3788
|
+
// Boîte noire : journal structuré + routage des lignes TF2EVENT des
|
|
3789
|
+
// sous-process (indexeur, migrations, Next, embeds) via le hook logger.
|
|
3790
|
+
setOpsJournalHooks({
|
|
3791
|
+
log: (scope, line) => log(scope, line),
|
|
3792
|
+
onAnomaly: (evt) => {
|
|
3793
|
+
void deps.vertical.reportCrashDebounced("ops-anomaly", evt.kind, {
|
|
3794
|
+
kind: evt.kind,
|
|
3795
|
+
outcome: evt.outcome,
|
|
3796
|
+
reason: evt.reason,
|
|
3797
|
+
ctx: evt.ctx,
|
|
3798
|
+
bootId: evt.bootId,
|
|
3799
|
+
});
|
|
3800
|
+
},
|
|
3801
|
+
});
|
|
3802
|
+
initOpsJournal(deps.paths.userDataDir(), app.getVersion());
|
|
3803
|
+
setOpsLineHandler(consumeOpsLine);
|
|
3804
|
+
track({ level: "event", kind: "app.start" });
|
|
3805
|
+
// Agent flotte DÈS le boot (hooks minimaux) — hôte uniquement : le Client
|
|
3806
|
+
// n'embarque pas host-runtime-ctx (paquet slim). Les hooks complets
|
|
3807
|
+
// remplacent ceux-ci en fin de boot local.
|
|
3808
|
+
if (deps.bootBehavior.allowLocalStack) {
|
|
3809
|
+
try {
|
|
3810
|
+
deps.hosts.fleetAgent().startFleetAgent({
|
|
3811
|
+
appKind: deps.appKind,
|
|
3812
|
+
getHealth: () => ({ next: `boot:${deps.vertical.getBootStage()}` }),
|
|
3813
|
+
executeRemoteCommand: async (cmd) => {
|
|
3814
|
+
if (cmd === "upload-diagnostics") {
|
|
3815
|
+
return deps.hosts.fleetAgent().uploadFleetDiagnostics("remote-command(boot)");
|
|
3816
|
+
}
|
|
3817
|
+
return { ok: false, detail: `boot en cours — commande différée: ${cmd}` };
|
|
3818
|
+
},
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
catch (e) {
|
|
3822
|
+
logError("fleet", e);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
app.on("child-process-gone", (_e, details) => {
|
|
3826
|
+
// GPU/utility process Chromium : rapport (fréquent sur certains drivers Windows).
|
|
3827
|
+
if (details.reason !== "clean-exit" && details.reason !== "killed") {
|
|
3828
|
+
deps.vertical.reportCrash("child-process-gone", { type: details.type, reason: details.reason });
|
|
3829
|
+
}
|
|
3830
|
+
});
|
|
3831
|
+
// Filet app-level : TOUT webContents (y compris popups OAuth, fenêtres
|
|
3832
|
+
// annexes) dont le rendu meurt est rapporté, même sans instrumentation
|
|
3833
|
+
// dédiée sur la vue.
|
|
3834
|
+
app.on("render-process-gone", (_e, contents, details) => {
|
|
3835
|
+
if (details.reason === "clean-exit")
|
|
3836
|
+
return;
|
|
3837
|
+
let url = "(?)";
|
|
3838
|
+
try {
|
|
3839
|
+
url = contents.isDestroyed() ? "(destroyed)" : contents.getURL();
|
|
3840
|
+
}
|
|
3841
|
+
catch {
|
|
3842
|
+
/* best-effort */
|
|
3843
|
+
}
|
|
3844
|
+
deps.vertical.reportCrash("renderer-gone", {
|
|
3845
|
+
view: "app-level",
|
|
3846
|
+
reason: details.reason,
|
|
3847
|
+
exitCode: details.exitCode,
|
|
3848
|
+
url,
|
|
3849
|
+
});
|
|
3850
|
+
});
|
|
3851
|
+
app.on("web-contents-created", (_e, contents) => {
|
|
3852
|
+
// Les webContents créés hors de notre code (popups OAuth des onglets
|
|
3853
|
+
// fournisseurs) reçoivent aussi la télémétrie de base. Différé d'un tick :
|
|
3854
|
+
// les vues instrumentées explicitement (crm, onglets) gardent leur label
|
|
3855
|
+
// précis, deps.vertical.instrumentWebContents est idempotent.
|
|
3856
|
+
setImmediate(() => {
|
|
3857
|
+
try {
|
|
3858
|
+
if (!contents.isDestroyed()) {
|
|
3859
|
+
deps.vertical.instrumentWebContents(contents, `wc-${contents.id}`);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
catch (err) {
|
|
3863
|
+
logError("web", err);
|
|
3864
|
+
}
|
|
3865
|
+
});
|
|
3866
|
+
});
|
|
3867
|
+
bootWithRetry().catch((e) => {
|
|
3868
|
+
logError("main", e);
|
|
3869
|
+
deps.vertical.reportCrash("boot-failure", {
|
|
3870
|
+
message: e instanceof Error ? e.message : String(e),
|
|
3871
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
3872
|
+
stage: deps.vertical.getBootStage(),
|
|
3873
|
+
});
|
|
3874
|
+
const dataRoot = deps.paths.userDataDir();
|
|
3875
|
+
dialog.showErrorBox(`${productName} — le démarrage a échoué`, `${e instanceof Error ? e.message : e}\n\n` +
|
|
3876
|
+
`Journal : ${crashLogHint() || `${dataRoot}\\logs`}\n` +
|
|
3877
|
+
`Rapports : ${crashReportsDir() || `${dataRoot}\\crash-reports`}\n` +
|
|
3878
|
+
`Données : ${dataRoot}\n\n` +
|
|
3879
|
+
`Un rapport a été enregistré localement et envoyé au support si le réseau est disponible.`);
|
|
3880
|
+
app.quit();
|
|
3881
|
+
});
|
|
3882
|
+
});
|
|
3883
|
+
function shutdown() {
|
|
3884
|
+
quitting = true;
|
|
3885
|
+
trayController?.destroy();
|
|
3886
|
+
trayController = null;
|
|
3887
|
+
aiScreencaster?.stopAll();
|
|
3888
|
+
aiScreencaster = null;
|
|
3889
|
+
bridge?.stop();
|
|
3890
|
+
// Fenêtres profils IA : vraie fermeture à l'arrêt (Q8).
|
|
3891
|
+
aiWorkspaces?.destroyAllWindows();
|
|
3892
|
+
closeAdminWindow();
|
|
3893
|
+
tabs?.closeAll();
|
|
3894
|
+
try {
|
|
3895
|
+
assistantChrome?.destroy();
|
|
3896
|
+
}
|
|
3897
|
+
catch {
|
|
3898
|
+
/* ignore */
|
|
3899
|
+
}
|
|
3900
|
+
assistantChrome = null;
|
|
3901
|
+
server?.stop();
|
|
3902
|
+
// Stack hôte : modules absents du paquet Client (host-stack lazy).
|
|
3903
|
+
if (deps.bootBehavior.allowLocalStack) {
|
|
3904
|
+
deps.hosts.tunnel().stopCloudflared();
|
|
3905
|
+
deps.hosts.plugins().stopAllPlugins();
|
|
3906
|
+
deps.hosts.pluginControl().stopPluginControlApi();
|
|
3907
|
+
deps.hosts.hermes().stopHermes();
|
|
3908
|
+
deps.hosts.n8n().stopN8n();
|
|
3909
|
+
}
|
|
3910
|
+
hermes = null;
|
|
3911
|
+
n8n = null;
|
|
3912
|
+
meili?.stop();
|
|
3913
|
+
try {
|
|
3914
|
+
deps.vertical.shutdownBrandRuntime();
|
|
3915
|
+
}
|
|
3916
|
+
catch {
|
|
3917
|
+
/* ignore */
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
app.on("before-quit", shutdown);
|
|
3921
|
+
app.on("window-all-closed", () => {
|
|
3922
|
+
shutdown();
|
|
3923
|
+
app.quit();
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
//# sourceMappingURL=brand-desktop-runtime.js.map
|