@creezio/shell-ui 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.
Files changed (232) hide show
  1. package/README.md +336 -0
  2. package/dist/adapters/nav-shell.d.ts +54 -0
  3. package/dist/adapters/nav-shell.d.ts.map +1 -0
  4. package/dist/adapters/nav-shell.js +112 -0
  5. package/dist/adapters/nav-shell.js.map +1 -0
  6. package/dist/brand.d.ts +64 -0
  7. package/dist/brand.d.ts.map +1 -0
  8. package/dist/brand.js +59 -0
  9. package/dist/brand.js.map +1 -0
  10. package/dist/core-nav.d.ts +17 -0
  11. package/dist/core-nav.d.ts.map +1 -0
  12. package/dist/core-nav.js +40 -0
  13. package/dist/core-nav.js.map +1 -0
  14. package/dist/index.d.ts +29 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +24 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/api-scopes.d.ts +24 -0
  19. package/dist/lib/api-scopes.d.ts.map +1 -0
  20. package/dist/lib/api-scopes.js +78 -0
  21. package/dist/lib/api-scopes.js.map +1 -0
  22. package/dist/lib/catalog-suspense-key.d.ts +3 -0
  23. package/dist/lib/catalog-suspense-key.d.ts.map +1 -0
  24. package/dist/lib/catalog-suspense-key.js +12 -0
  25. package/dist/lib/catalog-suspense-key.js.map +1 -0
  26. package/dist/lib/data-changed.d.ts +51 -0
  27. package/dist/lib/data-changed.d.ts.map +1 -0
  28. package/dist/lib/data-changed.js +227 -0
  29. package/dist/lib/data-changed.js.map +1 -0
  30. package/dist/lib/desktop-home-path.d.ts +14 -0
  31. package/dist/lib/desktop-home-path.d.ts.map +1 -0
  32. package/dist/lib/desktop-home-path.js +29 -0
  33. package/dist/lib/desktop-home-path.js.map +1 -0
  34. package/dist/lib/geo-distance.d.ts +6 -0
  35. package/dist/lib/geo-distance.d.ts.map +1 -0
  36. package/dist/lib/geo-distance.js +27 -0
  37. package/dist/lib/geo-distance.js.map +1 -0
  38. package/dist/lib/img.d.ts +2 -0
  39. package/dist/lib/img.d.ts.map +1 -0
  40. package/dist/lib/img.js +40 -0
  41. package/dist/lib/img.js.map +1 -0
  42. package/dist/lib/keepalive-eviction.d.ts +14 -0
  43. package/dist/lib/keepalive-eviction.d.ts.map +1 -0
  44. package/dist/lib/keepalive-eviction.js +44 -0
  45. package/dist/lib/keepalive-eviction.js.map +1 -0
  46. package/dist/lib/ops-track.d.ts +29 -0
  47. package/dist/lib/ops-track.d.ts.map +1 -0
  48. package/dist/lib/ops-track.js +37 -0
  49. package/dist/lib/ops-track.js.map +1 -0
  50. package/dist/lib/optimize-cover-url.d.ts +10 -0
  51. package/dist/lib/optimize-cover-url.d.ts.map +1 -0
  52. package/dist/lib/optimize-cover-url.js +80 -0
  53. package/dist/lib/optimize-cover-url.js.map +1 -0
  54. package/dist/lib/page-trails.d.ts +9 -0
  55. package/dist/lib/page-trails.d.ts.map +1 -0
  56. package/dist/lib/page-trails.js +20 -0
  57. package/dist/lib/page-trails.js.map +1 -0
  58. package/dist/lib/public-origin.d.ts +32 -0
  59. package/dist/lib/public-origin.d.ts.map +1 -0
  60. package/dist/lib/public-origin.js +125 -0
  61. package/dist/lib/public-origin.js.map +1 -0
  62. package/dist/lib/server-incident.d.ts +12 -0
  63. package/dist/lib/server-incident.d.ts.map +1 -0
  64. package/dist/lib/server-incident.js +72 -0
  65. package/dist/lib/server-incident.js.map +1 -0
  66. package/dist/lib/tab-document-url.d.ts +13 -0
  67. package/dist/lib/tab-document-url.d.ts.map +1 -0
  68. package/dist/lib/tab-document-url.js +60 -0
  69. package/dist/lib/tab-document-url.js.map +1 -0
  70. package/dist/lib/utils.d.ts +20 -0
  71. package/dist/lib/utils.d.ts.map +1 -0
  72. package/dist/lib/utils.js +87 -0
  73. package/dist/lib/utils.js.map +1 -0
  74. package/dist/nav-catalog.d.ts +97 -0
  75. package/dist/nav-catalog.d.ts.map +1 -0
  76. package/dist/nav-catalog.js +282 -0
  77. package/dist/nav-catalog.js.map +1 -0
  78. package/dist/registry.d.ts +11 -0
  79. package/dist/registry.d.ts.map +1 -0
  80. package/dist/registry.js +60 -0
  81. package/dist/registry.js.map +1 -0
  82. package/dist/types.d.ts +22 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +2 -0
  85. package/dist/types.js.map +1 -0
  86. package/dist-cjs/adapters/nav-shell.js +115 -0
  87. package/dist-cjs/adapters/nav-shell.js.map +1 -0
  88. package/dist-cjs/brand.js +66 -0
  89. package/dist-cjs/brand.js.map +1 -0
  90. package/dist-cjs/core-nav.js +43 -0
  91. package/dist-cjs/core-nav.js.map +1 -0
  92. package/dist-cjs/index.js +95 -0
  93. package/dist-cjs/index.js.map +1 -0
  94. package/dist-cjs/lib/api-scopes.js +86 -0
  95. package/dist-cjs/lib/api-scopes.js.map +1 -0
  96. package/dist-cjs/lib/catalog-suspense-key.js +15 -0
  97. package/dist-cjs/lib/catalog-suspense-key.js.map +1 -0
  98. package/dist-cjs/lib/data-changed.js +236 -0
  99. package/dist-cjs/lib/data-changed.js.map +1 -0
  100. package/dist-cjs/lib/desktop-home-path.js +34 -0
  101. package/dist-cjs/lib/desktop-home-path.js.map +1 -0
  102. package/dist-cjs/lib/geo-distance.js +30 -0
  103. package/dist-cjs/lib/geo-distance.js.map +1 -0
  104. package/dist-cjs/lib/img.js +43 -0
  105. package/dist-cjs/lib/img.js.map +1 -0
  106. package/dist-cjs/lib/keepalive-eviction.js +49 -0
  107. package/dist-cjs/lib/keepalive-eviction.js.map +1 -0
  108. package/dist-cjs/lib/ops-track.js +41 -0
  109. package/dist-cjs/lib/ops-track.js.map +1 -0
  110. package/dist-cjs/lib/optimize-cover-url.js +83 -0
  111. package/dist-cjs/lib/optimize-cover-url.js.map +1 -0
  112. package/dist-cjs/lib/page-trails.js +25 -0
  113. package/dist-cjs/lib/page-trails.js.map +1 -0
  114. package/dist-cjs/lib/public-origin.js +130 -0
  115. package/dist-cjs/lib/public-origin.js.map +1 -0
  116. package/dist-cjs/lib/server-incident.js +75 -0
  117. package/dist-cjs/lib/server-incident.js.map +1 -0
  118. package/dist-cjs/lib/tab-document-url.js +65 -0
  119. package/dist-cjs/lib/tab-document-url.js.map +1 -0
  120. package/dist-cjs/lib/utils.js +97 -0
  121. package/dist-cjs/lib/utils.js.map +1 -0
  122. package/dist-cjs/nav-catalog.js +293 -0
  123. package/dist-cjs/nav-catalog.js.map +1 -0
  124. package/dist-cjs/package.json +3 -0
  125. package/dist-cjs/registry.js +64 -0
  126. package/dist-cjs/registry.js.map +1 -0
  127. package/dist-cjs/types.js +3 -0
  128. package/dist-cjs/types.js.map +1 -0
  129. package/package.json +163 -0
  130. package/ui/app-error-boundary.tsx +59 -0
  131. package/ui/data-table.tsx +121 -0
  132. package/ui/desktop/auth-window-chrome.tsx +51 -0
  133. package/ui/desktop/desktop-bridge.tsx +87 -0
  134. package/ui/desktop/external-site-slot.tsx +313 -0
  135. package/ui/desktop/external-site-surface.ts +58 -0
  136. package/ui/desktop/site-link.tsx +105 -0
  137. package/ui/desktop/window-chrome-controls.tsx +5 -0
  138. package/ui/desktop-types.ts +30 -0
  139. package/ui/faceted-filters.tsx +119 -0
  140. package/ui/global-search-host.ts +20 -0
  141. package/ui/global-search.tsx +66 -0
  142. package/ui/index.ts +178 -0
  143. package/ui/kit.ts +30 -0
  144. package/ui/layout/app-shell.tsx +58 -0
  145. package/ui/layout/desktop-update-banner.tsx +100 -0
  146. package/ui/layout/entity-header.tsx +44 -0
  147. package/ui/layout/native-os-nav.ts +104 -0
  148. package/ui/layout/nav-catalog-loader.tsx +98 -0
  149. package/ui/layout/nav-icons.ts +67 -0
  150. package/ui/layout/page-chrome.tsx +90 -0
  151. package/ui/layout/page-toolbar-context.tsx +82 -0
  152. package/ui/layout/sandbox-banner.tsx +38 -0
  153. package/ui/layout/section-view-shell.tsx +73 -0
  154. package/ui/layout/sidebar-host.ts +144 -0
  155. package/ui/layout/sidebar.tsx +1150 -0
  156. package/ui/lib/ai-screencast-hub.ts +127 -0
  157. package/ui/lib/ai-workspace-client.ts +46 -0
  158. package/ui/lib/aid.ts +22 -0
  159. package/ui/lib/desktop-host.ts +114 -0
  160. package/ui/lib/fleet-tracker-client.ts +237 -0
  161. package/ui/lib/hermes-ui.ts +114 -0
  162. package/ui/lib/n8n-ui.ts +115 -0
  163. package/ui/lib/use-creezio-resource.tsx +121 -0
  164. package/ui/lib/use-shell-ui-brand.ts +22 -0
  165. package/ui/list-toolbar.tsx +309 -0
  166. package/ui/os-pages/desktop-settings-page.tsx +158 -0
  167. package/ui/os-pages/index.ts +8 -0
  168. package/ui/page-loading/entity-page-loading.tsx +59 -0
  169. package/ui/page-loading/list-page-loading.tsx +61 -0
  170. package/ui/pagination.tsx +56 -0
  171. package/ui/primitives/avatar.tsx +33 -0
  172. package/ui/primitives/badge.tsx +36 -0
  173. package/ui/primitives/breadcrumb.tsx +93 -0
  174. package/ui/primitives/button.tsx +44 -0
  175. package/ui/primitives/card.tsx +44 -0
  176. package/ui/primitives/chart.tsx +317 -0
  177. package/ui/primitives/command.tsx +111 -0
  178. package/ui/primitives/dialog.tsx +100 -0
  179. package/ui/primitives/dropdown-menu.tsx +187 -0
  180. package/ui/primitives/input.tsx +19 -0
  181. package/ui/primitives/label.tsx +17 -0
  182. package/ui/primitives/resizable.tsx +45 -0
  183. package/ui/primitives/scroll-area.tsx +41 -0
  184. package/ui/primitives/select.tsx +73 -0
  185. package/ui/primitives/separator.tsx +26 -0
  186. package/ui/primitives/sheet.tsx +90 -0
  187. package/ui/primitives/skeleton.tsx +7 -0
  188. package/ui/primitives/sonner.tsx +22 -0
  189. package/ui/primitives/tabs.tsx +51 -0
  190. package/ui/primitives/textarea.tsx +22 -0
  191. package/ui/primitives/tooltip.tsx +31 -0
  192. package/ui/pwa/client-error-reporter.tsx +62 -0
  193. package/ui/pwa/register-sw.tsx +50 -0
  194. package/ui/range-filters.tsx +174 -0
  195. package/ui/search/global-search-config.ts +32 -0
  196. package/ui/search/global-search-provider.tsx +528 -0
  197. package/ui/search/search-history.ts +74 -0
  198. package/ui/search-input.tsx +146 -0
  199. package/ui/settings/account-settings.tsx +140 -0
  200. package/ui/settings/agent-profile-settings.tsx +184 -0
  201. package/ui/settings/api-keys-settings.tsx +349 -0
  202. package/ui/settings/desktop-background-settings.tsx +128 -0
  203. package/ui/settings/desktop-connection-settings.tsx +251 -0
  204. package/ui/settings/desktop-embed-env-panel.tsx +257 -0
  205. package/ui/settings/desktop-fleet-telemetry-settings.tsx +269 -0
  206. package/ui/settings/desktop-hermes-settings.tsx +341 -0
  207. package/ui/settings/desktop-llm-keys.tsx +192 -0
  208. package/ui/settings/desktop-n8n-settings.tsx +306 -0
  209. package/ui/settings/desktop-tunnel.tsx +264 -0
  210. package/ui/settings/desktop-update-settings.tsx +175 -0
  211. package/ui/settings/factory-reset-settings.tsx +89 -0
  212. package/ui/settings/host-only-settings.tsx +62 -0
  213. package/ui/settings/locked-config-field.tsx +30 -0
  214. package/ui/settings/ops-diagnostic-settings.tsx +242 -0
  215. package/ui/settings/search-reindex-settings.tsx +158 -0
  216. package/ui/settings/server-mode-cards.tsx +801 -0
  217. package/ui/theme/tailwind-preset.cjs +82 -0
  218. package/ui/theme/theme.css +328 -0
  219. package/ui/workspace/ai-activity-panel-host.tsx +19 -0
  220. package/ui/workspace/ai-workspace-agent-host.tsx +54 -0
  221. package/ui/workspace/ai-workspace-banner.tsx +168 -0
  222. package/ui/workspace/impersonation-banner.tsx +39 -0
  223. package/ui/workspace/keep-alive.tsx +220 -0
  224. package/ui/workspace/tab-workspace-context.tsx +1172 -0
  225. package/ui/workspace/tab-workspace-host.ts +89 -0
  226. package/ui/workspace/types.ts +500 -0
  227. package/ui/workspace/use-location-search.tsx +50 -0
  228. package/ui/workspace/window-chrome-controls.tsx +116 -0
  229. package/ui/workspace/workspace-config.ts +47 -0
  230. package/ui/workspace/workspace-root.tsx +147 -0
  231. package/ui/workspace/workspace-shell.tsx +292 -0
  232. package/ui/workspace/workspace-tab-bar.tsx +572 -0
@@ -0,0 +1,192 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * BYOK (Bring Your Own Key) — app desktop uniquement.
7
+ *
8
+ * Mode local mono-client : BYOK strict. Sans clé OpenAI utilisateur
9
+ * l'assistant est désactivé. Aucune clé éditeur n'est packagée ni injectée.
10
+ * Après enregistrement : Next + Hermes sont redémarrés pour injecter OPENAI/ANTHROPIC.
11
+ * Rendu null hors app desktop (getShellDesktopApi() absent).
12
+ */
13
+
14
+ import { useEffect, useState } from "react";
15
+ import { toast } from "sonner";
16
+ import { KeyRound, Loader2 } from "lucide-react";
17
+ import { Button } from "../primitives/button";
18
+ import { Input } from "../primitives/input";
19
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
20
+ import { ServerLlmKeysCard } from "./server-mode-cards";
21
+
22
+ type Provider = "openai" | "anthropic";
23
+
24
+ type LlmStatus = {
25
+ openai: boolean;
26
+ anthropic: boolean;
27
+ serverOpenAi: boolean;
28
+ serverAnthropic: boolean;
29
+ assistantReady: boolean;
30
+ restarting: boolean;
31
+ };
32
+
33
+ const LABELS: Record<Provider, { name: string; placeholder: string; required: boolean }> = {
34
+ openai: { name: "OpenAI", placeholder: "sk-…", required: true },
35
+ anthropic: { name: "Anthropic (optionnel)", placeholder: "sk-ant-…", required: false },
36
+ };
37
+
38
+ function statusLabel(stored: boolean, active: boolean): string {
39
+ if (stored && active) return "active";
40
+ if (stored && !active) return "enregistrée — activation…";
41
+ return "non configuré";
42
+ }
43
+
44
+ export function DesktopLlmKeys() {
45
+ const [desktop, setDesktop] = useState(false);
46
+ const [status, setStatus] = useState<LlmStatus>({
47
+ openai: false,
48
+ anthropic: false,
49
+ serverOpenAi: false,
50
+ serverAnthropic: false,
51
+ assistantReady: false,
52
+ restarting: false,
53
+ });
54
+ const [drafts, setDrafts] = useState<Record<Provider, string>>({ openai: "", anthropic: "" });
55
+ const [saving, setSaving] = useState<Provider | null>(null);
56
+
57
+ useEffect(() => {
58
+ const api = getShellDesktopApi();
59
+ if (!api) return;
60
+ setDesktop(true);
61
+ void api.getLlmKeyStatus().then(setStatus);
62
+ const unsub = api.onLlmStatusChanged?.(setStatus);
63
+ return () => unsub?.();
64
+ }, []);
65
+
66
+ // Web (serveur headless) : GET/PUT /api/v1/platform/llm-keys (owner).
67
+ if (!desktop) return <ServerLlmKeysCard />;
68
+
69
+ async function save(provider: Provider) {
70
+ const api = getShellDesktopApi();
71
+ if (!api) return;
72
+ const key = drafts[provider].trim();
73
+ setSaving(provider);
74
+ try {
75
+ const result = await api.setLlmKey(provider, key || null);
76
+ setStatus(result.status);
77
+ setDrafts((d) => ({ ...d, [provider]: "" }));
78
+ if (!result.ok) {
79
+ toast.error(
80
+ result.error ||
81
+ `Clé ${LABELS[provider].name} enregistrée, mais le serveur n'a pas pu redémarrer.`,
82
+ );
83
+ return;
84
+ }
85
+ if (provider === "openai") {
86
+ toast.success(
87
+ key
88
+ ? result.status.assistantReady
89
+ ? "Clé OpenAI active — l'assistant est prêt."
90
+ : "Clé OpenAI enregistrée."
91
+ : "Clé OpenAI supprimée — assistant désactivé.",
92
+ );
93
+ } else {
94
+ toast.success(
95
+ key
96
+ ? "Clé Anthropic active (fallback quota OpenAI)."
97
+ : "Clé Anthropic supprimée.",
98
+ );
99
+ }
100
+ } catch (e) {
101
+ toast.error(e instanceof Error ? e.message : "Erreur d'enregistrement de la clé");
102
+ } finally {
103
+ setSaving(null);
104
+ try {
105
+ setStatus(await api.getLlmKeyStatus());
106
+ } catch {
107
+ /* ignore */
108
+ }
109
+ }
110
+ }
111
+
112
+ const bannerClass = status.restarting
113
+ ? "rounded-md border border-sky-200 bg-sky-50 px-3 py-2 text-sm text-sky-900"
114
+ : status.assistantReady
115
+ ? "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800"
116
+ : "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900";
117
+
118
+ const bannerText = status.restarting
119
+ ? "Assistant : activation en cours (redémarrage du serveur local)…"
120
+ : status.assistantReady
121
+ ? "Assistant : prêt (clé OpenAI active côté serveur)"
122
+ : status.openai && !status.serverOpenAi
123
+ ? "Assistant : clé OpenAI enregistrée mais pas encore active — relancez l'enregistrement ou redémarrez Creezio."
124
+ : "Assistant : désactivé — clé OpenAI requise (BYOK)";
125
+
126
+ return (
127
+ <Card>
128
+ <CardHeader>
129
+ <CardTitle className="flex items-center gap-2 text-base">
130
+ <KeyRound className="h-4 w-4" /> Clés IA (Bring Your Own Key)
131
+ </CardTitle>
132
+ <CardDescription>
133
+ Une clé OpenAI est obligatoire pour l&apos;assistant et Hermes. Anthropic est optionnel
134
+ (secours si quota OpenAI). Les clés sont chiffrées sur cet ordinateur et ne sont jamais
135
+ envoyées ailleurs que chez le fournisseur du modèle. Après enregistrement, l&apos;app
136
+ redémarre le serveur local et Hermes pour propager les clés.
137
+ </CardDescription>
138
+ </CardHeader>
139
+ <CardContent className="space-y-3">
140
+ <p className={bannerClass}>{bannerText}</p>
141
+ {(Object.keys(LABELS) as Provider[]).map((provider) => {
142
+ const stored = status[provider];
143
+ const active = provider === "openai" ? status.serverOpenAi : status.serverAnthropic;
144
+ const busy = saving === provider || status.restarting;
145
+ return (
146
+ <div key={provider} className="flex items-center gap-2">
147
+ <span className="w-44 shrink-0 text-sm">
148
+ {LABELS[provider].name}
149
+ {LABELS[provider].required ? (
150
+ <span className="ml-1 text-xs text-amber-700">requis</span>
151
+ ) : null}
152
+ <span
153
+ className={
154
+ stored && active
155
+ ? "ml-1 text-xs text-emerald-600"
156
+ : "ml-1 text-xs text-muted-foreground"
157
+ }
158
+ >
159
+ ({statusLabel(stored, active)})
160
+ </span>
161
+ </span>
162
+ <Input
163
+ type="password"
164
+ value={drafts[provider]}
165
+ onChange={(e) => setDrafts((d) => ({ ...d, [provider]: e.target.value }))}
166
+ placeholder={stored ? "•••••• (remplacer)" : LABELS[provider].placeholder}
167
+ className="flex-1"
168
+ disabled={busy}
169
+ />
170
+ <Button
171
+ size="sm"
172
+ variant="outline"
173
+ disabled={busy}
174
+ onClick={() => void save(provider)}
175
+ >
176
+ {busy ? (
177
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
178
+ ) : drafts[provider].trim() ? (
179
+ "Enregistrer"
180
+ ) : stored ? (
181
+ "Supprimer"
182
+ ) : (
183
+ "Enregistrer"
184
+ )}
185
+ </Button>
186
+ </div>
187
+ );
188
+ })}
189
+ </CardContent>
190
+ </Card>
191
+ );
192
+ }
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * n8n — stack native Creezio.
7
+ * Statut + paramètres verrouillés (tunnel/webhooks) + actions.
8
+ */
9
+
10
+ import { useCallback, useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import {
13
+ Workflow,
14
+ Wrench,
15
+ ExternalLink,
16
+ Loader2,
17
+ RefreshCw,
18
+ } from "lucide-react";
19
+ import { Button } from "../primitives/button";
20
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
21
+ import { useTabWorkspaceOptional } from "../workspace/tab-workspace-host";
22
+ import { openN8nUiInWorkspace } from "../lib/n8n-ui";
23
+ import { LockedConfigField } from "./locked-config-field";
24
+ import { DesktopEmbedEnvPanel } from "./desktop-embed-env-panel";
25
+ import { ServerServiceStatusCard } from "./server-mode-cards";
26
+
27
+ type N8nStatus = {
28
+ status: string;
29
+ mode: string;
30
+ uiUrl: string | null;
31
+ entryFound: boolean;
32
+ entryPath: string | null;
33
+ version: string | null;
34
+ detail: string;
35
+ homeDir: string | null;
36
+ bootstrapPhase: string;
37
+ bootstrapError: string | null;
38
+ installing: boolean;
39
+ ownerReady: boolean;
40
+ logs: string[];
41
+ localUiUrl?: string | null;
42
+ publicWebhookUrl?: string | null;
43
+ listenHost?: string;
44
+ listenPort?: number;
45
+ };
46
+
47
+ type TunnelStatus = {
48
+ configured?: boolean;
49
+ publicUrls?: { n8n?: string | null } | null;
50
+ };
51
+
52
+ function statusTone(status: string): string {
53
+ if (status === "running") {
54
+ return "border-emerald-200 bg-emerald-50 text-emerald-900";
55
+ }
56
+ if (status === "installing") {
57
+ return "border-sky-200 bg-sky-50 text-sky-900";
58
+ }
59
+ if (status === "missing" || status === "error") {
60
+ return "border-amber-200 bg-amber-50 text-amber-900";
61
+ }
62
+ if (status === "skipped-remote-client" || status === "remote") {
63
+ return "border-sky-200 bg-sky-50 text-sky-900";
64
+ }
65
+ return "border-slate-200 bg-slate-50 text-slate-800";
66
+ }
67
+
68
+ function statusLabel(status: string): string {
69
+ switch (status) {
70
+ case "running":
71
+ return "En cours";
72
+ case "stopped":
73
+ return "Arrêté";
74
+ case "missing":
75
+ return "Premier démarrage…";
76
+ case "installing":
77
+ return "Préparation…";
78
+ case "error":
79
+ return "Erreur";
80
+ case "skipped-remote-client":
81
+ return "Via l’hôte (Rejoindre)";
82
+ case "remote":
83
+ return "Via tunnel hôte";
84
+ default:
85
+ return status;
86
+ }
87
+ }
88
+
89
+ export function DesktopN8nSettings() {
90
+ const workspace = useTabWorkspaceOptional();
91
+ const [desktop, setDesktop] = useState(false);
92
+ const [loading, setLoading] = useState(true);
93
+ const [installing, setInstalling] = useState(false);
94
+ const [status, setStatus] = useState<N8nStatus | null>(null);
95
+ const [tunnelN8n, setTunnelN8n] = useState<string | null>(null);
96
+ const [showLogs, setShowLogs] = useState(false);
97
+
98
+ const refresh = useCallback(async () => {
99
+ const api = getShellDesktopApi();
100
+ if (!api?.getN8nStatus) return;
101
+ setLoading(true);
102
+ try {
103
+ setStatus((await api.getN8nStatus()) as N8nStatus);
104
+ if (api.getTunnelStatus) {
105
+ const t = (await api.getTunnelStatus()) as TunnelStatus;
106
+ setTunnelN8n(t.publicUrls?.n8n || null);
107
+ }
108
+ } finally {
109
+ setLoading(false);
110
+ }
111
+ }, []);
112
+
113
+ useEffect(() => {
114
+ const api = getShellDesktopApi();
115
+ if (!api?.getN8nStatus) return;
116
+ setDesktop(true);
117
+ void refresh();
118
+ }, [refresh]);
119
+
120
+ // Web (serveur headless) : statut via /api/v1/os/n8n/status.
121
+ if (!desktop) return <ServerServiceStatusCard kind="n8n" />;
122
+
123
+ async function onOpenUiTab() {
124
+ const r = await openN8nUiInWorkspace({
125
+ openExternalSite: workspace?.openExternalSite
126
+ ? (o) => workspace.openExternalSite(o)
127
+ : undefined,
128
+ openTab: getShellDesktopApi()?.openTab
129
+ ? (siteId, url) => getShellDesktopApi()!.openTab!(siteId, url)
130
+ : undefined,
131
+ });
132
+ if (r.ok) toast.success("Onglet n8n ouvert");
133
+ else toast.error(r.error);
134
+ }
135
+
136
+ async function onRepairRuntime() {
137
+ const api = getShellDesktopApi();
138
+ if (!api?.ensureN8nRuntime) return;
139
+ setInstalling(true);
140
+ try {
141
+ const r = await api.ensureN8nRuntime();
142
+ if (r.ok) toast.success(r.detail || "Runtime n8n OK");
143
+ else toast.error(r.detail || "Réparation échouée");
144
+ await refresh();
145
+ } catch (e) {
146
+ toast.error(e instanceof Error ? e.message : "Erreur");
147
+ } finally {
148
+ setInstalling(false);
149
+ }
150
+ }
151
+
152
+ const busy = installing || Boolean(status?.installing);
153
+ const localUrl =
154
+ status?.localUiUrl || status?.uiUrl || "http://127.0.0.1:15678";
155
+ const webhookUrl =
156
+ status?.publicWebhookUrl ||
157
+ (tunnelN8n ? `${tunnelN8n.replace(/\/$/, "")}/` : null);
158
+ const webhookOk =
159
+ Boolean(webhookUrl) && !webhookUrl!.includes("127.0.0.1");
160
+
161
+ return (
162
+ <Card>
163
+ <CardHeader>
164
+ <CardTitle className="flex items-center gap-2 text-base">
165
+ <Workflow className="h-4 w-4" /> n8n
166
+ </CardTitle>
167
+ <CardDescription>
168
+ Automatisations natives Creezio. Les webhooks doivent utiliser
169
+ l’URL tunnel (pas l’IP locale).
170
+ </CardDescription>
171
+ </CardHeader>
172
+ <CardContent className="space-y-4">
173
+ <div
174
+ className={`rounded-md border px-3 py-2 text-sm ${statusTone(status?.status || "")}`}
175
+ >
176
+ {loading || !status ? (
177
+ <span className="inline-flex items-center gap-2">
178
+ <Loader2 className="h-3.5 w-3.5 animate-spin" /> Chargement…
179
+ </span>
180
+ ) : (
181
+ <>
182
+ <div className="font-medium">
183
+ {statusLabel(status.status)}
184
+ {status.version ? ` · v${status.version}` : ""}
185
+ </div>
186
+ <div className="mt-0.5 text-xs opacity-90">{status.detail}</div>
187
+ {status.ownerReady ? (
188
+ <div className="mt-1 text-xs opacity-90">
189
+ Connexion automatique active
190
+ </div>
191
+ ) : null}
192
+ {status.bootstrapError && status.status !== "running" ? (
193
+ <div className="mt-1 text-xs text-amber-800">
194
+ {status.bootstrapError}
195
+ </div>
196
+ ) : null}
197
+ </>
198
+ )}
199
+ </div>
200
+
201
+ <div>
202
+ <div className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
203
+ Service (pré-configuré)
204
+ </div>
205
+ <div className="grid gap-2 sm:grid-cols-2">
206
+ <LockedConfigField
207
+ label="Mode"
208
+ value="Embarqué (natif Creezio)"
209
+ hint="Toujours actif sur Héberger — non dissociable."
210
+ />
211
+ <LockedConfigField
212
+ label="Écoute locale"
213
+ value={`${status?.listenHost || "127.0.0.1"}:${status?.listenPort || 15678}`}
214
+ hint="Loopback uniquement — jamais exposé directement."
215
+ />
216
+ <LockedConfigField
217
+ label="URL locale (onglet desktop)"
218
+ value={localUrl}
219
+ />
220
+ <LockedConfigField
221
+ label="URL publique webhooks / editor"
222
+ value={webhookUrl || "Tunnel non réservé"}
223
+ hint={
224
+ webhookOk
225
+ ? "WEBHOOK_URL et N8N_EDITOR_BASE_URL — c’est cette adresse que les webhooks doivent afficher."
226
+ : "Réservez un slug dans Accès distant, puis redémarrez si besoin. Sans tunnel = IP locale uniquement."
227
+ }
228
+ />
229
+ <LockedConfigField
230
+ label="Dossier données"
231
+ value={status?.homeDir || "…/n8n-home"}
232
+ hint="Sous le profil Creezio (OS sandbox)."
233
+ />
234
+ <LockedConfigField
235
+ label="Auth"
236
+ value="Owner silencieux (cookie session)"
237
+ hint="Pas de mot de passe à saisir dans l’UI."
238
+ />
239
+ <LockedConfigField
240
+ label="MCP instance"
241
+ value="Activé (N8N_MCP_ACCESS_ENABLED)"
242
+ hint="Géré par Creezio — Hermes utilise aussi l’API key REST."
243
+ />
244
+ <LockedConfigField
245
+ label="API key Hermes"
246
+ value="Provisionnée silencieusement"
247
+ hint="Fichier n8n-home/.creezio-n8n-api-key.json — injectée dans Hermes."
248
+ />
249
+ </div>
250
+ </div>
251
+
252
+ <DesktopEmbedEnvPanel service="n8n" />
253
+
254
+ <div className="flex flex-wrap gap-2">
255
+ <Button
256
+ size="sm"
257
+ variant="secondary"
258
+ onClick={() => void onOpenUiTab()}
259
+ disabled={busy || status?.status === "skipped-remote-client"}
260
+ >
261
+ {installing || status?.installing ? (
262
+ <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
263
+ ) : (
264
+ <ExternalLink className="mr-1 h-3.5 w-3.5" />
265
+ )}
266
+ Ouvrir dans un onglet
267
+ </Button>
268
+ <Button
269
+ size="sm"
270
+ variant="outline"
271
+ onClick={() => void refresh()}
272
+ disabled={loading}
273
+ >
274
+ <RefreshCw className="mr-1 h-3.5 w-3.5" /> Actualiser
275
+ </Button>
276
+ <Button
277
+ size="sm"
278
+ variant="ghost"
279
+ onClick={() => setShowLogs((v) => !v)}
280
+ >
281
+ {showLogs ? "Masquer les logs" : "Logs"}
282
+ </Button>
283
+ <Button
284
+ size="sm"
285
+ variant="outline"
286
+ onClick={() => void onRepairRuntime()}
287
+ disabled={busy || status?.status === "skipped-remote-client"}
288
+ >
289
+ {installing || status?.installing ? (
290
+ <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
291
+ ) : (
292
+ <Wrench className="mr-1 h-3.5 w-3.5" />
293
+ )}
294
+ Réparer
295
+ </Button>
296
+ </div>
297
+
298
+ {showLogs && status?.logs?.length ? (
299
+ <pre className="max-h-48 overflow-auto rounded-md border bg-slate-950 p-2 font-mono text-[11px] leading-relaxed text-slate-100">
300
+ {status.logs.join("\n")}
301
+ </pre>
302
+ ) : null}
303
+ </CardContent>
304
+ </Card>
305
+ );
306
+ }