@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,269 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Configuration → Support & télémétrie — consentement granulaire flotte.
7
+ */
8
+
9
+ import { useCallback, useEffect, useState } from "react";
10
+ import { toast } from "sonner";
11
+ import { Loader2, Shield } from "lucide-react";
12
+ import { Button } from "../primitives/button";
13
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
14
+ import { Label } from "../primitives/label";
15
+ import { ServerOperatorNotice } from "./server-mode-cards";
16
+
17
+ type FleetScopeId =
18
+ | "heartbeat"
19
+ | "crashes"
20
+ | "ops"
21
+ | "sessions"
22
+ | "users"
23
+ | "request_logs"
24
+ | "hermes_stats"
25
+ | "hermes_chats"
26
+ | "assistant_chats"
27
+ | "plugins"
28
+ | "actions"
29
+ | "remote_commands";
30
+
31
+ type FleetTelemetryConfig = {
32
+ enabled: boolean;
33
+ scopes: Record<FleetScopeId, boolean>;
34
+ consentAt: string | null;
35
+ consentVersion: number;
36
+ };
37
+
38
+ const SCOPE_META: Array<{
39
+ id: FleetScopeId;
40
+ label: string;
41
+ detail: string;
42
+ sensitive?: boolean;
43
+ }> = [
44
+ {
45
+ id: "heartbeat",
46
+ label: "Présence & santé",
47
+ detail: "Online, version, slug, état des services locaux",
48
+ },
49
+ {
50
+ id: "crashes",
51
+ label: "Incidents / crashes",
52
+ detail: "Rapports de crash vers le cockpit support",
53
+ },
54
+ {
55
+ id: "ops",
56
+ label: "Boîte noire (boot & logs)",
57
+ detail: "Décisions de boot, durées, anomalies + tail du log principal — redacté",
58
+ },
59
+ {
60
+ id: "sessions",
61
+ label: "Sessions collaborateurs",
62
+ detail: "Qui est connecté (jamais les mots de passe)",
63
+ },
64
+ {
65
+ id: "users",
66
+ label: "Comptes locaux",
67
+ detail: "Liste users + rôles — pas de secrets / recovery",
68
+ },
69
+ {
70
+ id: "request_logs",
71
+ label: "Logs API / MCP",
72
+ detail: "Échantillon redacté (Authorization / BYOK masqués)",
73
+ },
74
+ {
75
+ id: "hermes_stats",
76
+ label: "Hermes — stats",
77
+ detail: "Compteurs appels / erreurs / latence",
78
+ },
79
+ {
80
+ id: "hermes_chats",
81
+ label: "Hermes — contenu des chats",
82
+ detail: "Messages Work / sessions Hermes",
83
+ sensitive: true,
84
+ },
85
+ {
86
+ id: "assistant_chats",
87
+ label: "Assistant in-app — chats",
88
+ detail: "Conversations assistant",
89
+ sensitive: true,
90
+ },
91
+ {
92
+ id: "plugins",
93
+ label: "Plugins",
94
+ detail: "Inventaire manifest + état",
95
+ },
96
+ {
97
+ id: "actions",
98
+ label: "Journal d’actions",
99
+ detail: "Événements UI / commandes métier agrégés",
100
+ },
101
+ {
102
+ id: "remote_commands",
103
+ label: "Accepter le pilotage distant",
104
+ detail: "Allowlist : update-check, restart embed, envoi diagnostics (pas de shell)",
105
+ },
106
+ ];
107
+
108
+ export function DesktopFleetTelemetrySettings() {
109
+ const [desktop, setDesktop] = useState(false);
110
+ const [loading, setLoading] = useState(true);
111
+ const [busy, setBusy] = useState(false);
112
+ const [cfg, setCfg] = useState<FleetTelemetryConfig | null>(null);
113
+
114
+ const refresh = useCallback(async () => {
115
+ const api = getShellDesktopApi();
116
+ if (!api?.getFleetTelemetry) return;
117
+ try {
118
+ setCfg(await api.getFleetTelemetry());
119
+ } catch {
120
+ // Client distant : canal host-only refusé — la section est masquée
121
+ // par HostOnlySettings, ne pas laisser un rejet non catché au montage.
122
+ }
123
+ }, []);
124
+
125
+ useEffect(() => {
126
+ const api = getShellDesktopApi();
127
+ if (!api?.getFleetTelemetry) {
128
+ setLoading(false);
129
+ return;
130
+ }
131
+ setDesktop(true);
132
+ void refresh().finally(() => setLoading(false));
133
+ }, [refresh]);
134
+
135
+ async function apply(patch: {
136
+ enabled?: boolean;
137
+ scopes?: Partial<Record<FleetScopeId, boolean>>;
138
+ preset?: "basic" | "off" | "keep";
139
+ }) {
140
+ const api = getShellDesktopApi();
141
+ if (!api?.setFleetTelemetry) return;
142
+ setBusy(true);
143
+ try {
144
+ const next = await api.setFleetTelemetry(patch);
145
+ setCfg(next);
146
+ toast.success("Préférences télémétrie enregistrées");
147
+ } catch (e) {
148
+ toast.error(e instanceof Error ? e.message : "Échec enregistrement");
149
+ } finally {
150
+ setBusy(false);
151
+ }
152
+ }
153
+
154
+ async function onScope(id: FleetScopeId, value: boolean) {
155
+ const meta = SCOPE_META.find((m) => m.id === id);
156
+ if (value && meta?.sensitive) {
157
+ const ok = window.confirm(
158
+ "Je comprends que le contenu des conversations sera visible par le support Creezio.",
159
+ );
160
+ if (!ok) return;
161
+ }
162
+ await apply({ scopes: { [id]: value } });
163
+ }
164
+
165
+ // Web (serveur headless) : consentement posé par l'opérateur de l'instance.
166
+ if (!desktop) return <ServerOperatorNotice label="la télémétrie support" />;
167
+ if (loading || !cfg) {
168
+ return (
169
+ <Card>
170
+ <CardContent className="flex items-center gap-2 py-8 text-sm text-muted-foreground">
171
+ <Loader2 className="h-4 w-4 animate-spin" /> Chargement…
172
+ </CardContent>
173
+ </Card>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <Card>
179
+ <CardHeader>
180
+ <CardTitle className="flex items-center gap-2 text-base">
181
+ <Shield className="h-4 w-4" />
182
+ Support & télémétrie
183
+ </CardTitle>
184
+ <CardDescription>
185
+ Partage optionnel avec Creezio Support. Tout est désactivé par défaut.
186
+ Les secrets (clés IA, JWT, tokens tunnel, mots de passe) ne sont jamais
187
+ envoyés.
188
+ </CardDescription>
189
+ </CardHeader>
190
+ <CardContent className="space-y-6">
191
+ <label className="flex cursor-pointer items-start justify-between gap-4 rounded-md border px-3 py-3">
192
+ <div className="space-y-1">
193
+ <Label className="text-sm font-medium">
194
+ Partager des données avec Creezio Support
195
+ </Label>
196
+ <p className="text-xs text-muted-foreground">
197
+ {cfg.enabled
198
+ ? cfg.consentAt
199
+ ? `Actif depuis ${new Date(cfg.consentAt).toLocaleString("fr-FR")}`
200
+ : "Actif"
201
+ : "Aucun envoi vers le cockpit flotte tant que c’est désactivé"}
202
+ </p>
203
+ </div>
204
+ <input
205
+ type="checkbox"
206
+ className="mt-1 h-4 w-4"
207
+ checked={cfg.enabled}
208
+ disabled={busy}
209
+ onChange={(e) => void apply({ enabled: e.target.checked })}
210
+ />
211
+ </label>
212
+
213
+ <div className="flex flex-wrap gap-2">
214
+ <Button
215
+ type="button"
216
+ size="sm"
217
+ variant="secondary"
218
+ disabled={busy}
219
+ onClick={() => void apply({ preset: "basic" })}
220
+ >
221
+ Activer le support basique
222
+ </Button>
223
+ <Button
224
+ type="button"
225
+ size="sm"
226
+ variant="outline"
227
+ disabled={busy}
228
+ onClick={() => void apply({ preset: "off" })}
229
+ >
230
+ Tout désactiver
231
+ </Button>
232
+ </div>
233
+
234
+ <div
235
+ className={`space-y-3 ${cfg.enabled ? "" : "pointer-events-none opacity-50"}`}
236
+ >
237
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
238
+ Catégories
239
+ </p>
240
+ {SCOPE_META.map((m) => (
241
+ <label
242
+ key={m.id}
243
+ className="flex cursor-pointer items-start justify-between gap-4 border-b border-border/60 pb-3 last:border-0"
244
+ >
245
+ <div className="min-w-0 space-y-0.5">
246
+ <span className="text-sm font-medium">
247
+ {m.label}
248
+ {m.sensitive ? (
249
+ <span className="ml-2 text-[10px] font-normal uppercase text-amber-700">
250
+ sensible
251
+ </span>
252
+ ) : null}
253
+ </span>
254
+ <p className="text-xs text-muted-foreground">{m.detail}</p>
255
+ </div>
256
+ <input
257
+ type="checkbox"
258
+ className="mt-1 h-4 w-4"
259
+ checked={cfg.scopes[m.id] === true}
260
+ disabled={busy || !cfg.enabled}
261
+ onChange={(e) => void onScope(m.id, e.target.checked)}
262
+ />
263
+ </label>
264
+ ))}
265
+ </div>
266
+ </CardContent>
267
+ </Card>
268
+ );
269
+ }
@@ -0,0 +1,341 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Hermes — stack native Creezio (toujours embarqué sur Héberger).
7
+ * Statut + ouverture WebUI + logs. Pas de mode distant/désactivé.
8
+ */
9
+
10
+ import { useCallback, useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import { Bot, Download, ExternalLink, Loader2, RefreshCw } from "lucide-react";
13
+ import { Button } from "../primitives/button";
14
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
15
+ import { useTabWorkspaceOptional } from "../workspace/tab-workspace-host";
16
+ import {
17
+ isHermesWebuiOpenTarget,
18
+ resolveHermesWebuiOpenTarget,
19
+ } from "../lib/hermes-ui";
20
+ import { LockedConfigField } from "./locked-config-field";
21
+ import { DesktopEmbedEnvPanel } from "./desktop-embed-env-panel";
22
+ import { ServerServiceStatusCard } from "./server-mode-cards";
23
+
24
+ type HermesStatus = {
25
+ status: string;
26
+ mode: string;
27
+ apiUrl: string | null;
28
+ webuiUrl: string | null;
29
+ webuiStatus: string;
30
+ binaryFound: boolean;
31
+ binaryPath: string | null;
32
+ version: string | null;
33
+ detail: string;
34
+ homeDir: string | null;
35
+ bootstrapPhase: string;
36
+ bootstrapError: string | null;
37
+ installing: boolean;
38
+ logs: string[];
39
+ };
40
+
41
+ function statusTone(status: string): string {
42
+ if (status === "running") {
43
+ return "border-emerald-200 bg-emerald-50 text-emerald-900";
44
+ }
45
+ if (status === "installing" || status === "starting") {
46
+ return "border-sky-200 bg-sky-50 text-sky-900";
47
+ }
48
+ if (status === "missing" || status === "error") {
49
+ return "border-amber-200 bg-amber-50 text-amber-900";
50
+ }
51
+ if (status === "skipped-remote-client" || status === "remote") {
52
+ return "border-sky-200 bg-sky-50 text-sky-900";
53
+ }
54
+ return "border-slate-200 bg-slate-50 text-slate-800";
55
+ }
56
+
57
+ function statusLabel(status: string): string {
58
+ switch (status) {
59
+ case "running":
60
+ return "En cours";
61
+ case "stopped":
62
+ return "Arrêté";
63
+ case "missing":
64
+ return "Runtime absent";
65
+ case "installing":
66
+ return "Installation…";
67
+ case "starting":
68
+ return "Démarrage…";
69
+ case "error":
70
+ return "Erreur";
71
+ case "skipped-remote-client":
72
+ return "Via l’hôte (Rejoindre)";
73
+ case "remote":
74
+ return "Via tunnel hôte";
75
+ default:
76
+ return status;
77
+ }
78
+ }
79
+
80
+ function webuiLabel(status: string): string {
81
+ switch (status) {
82
+ case "running":
83
+ return "WebUI kanban actif";
84
+ case "missing":
85
+ return "WebUI absent";
86
+ case "error":
87
+ return "WebUI erreur";
88
+ case "skipped":
89
+ return "WebUI via tunnel";
90
+ case "stopped":
91
+ return "WebUI arrêté";
92
+ default:
93
+ return status || "—";
94
+ }
95
+ }
96
+
97
+ export function DesktopHermesSettings() {
98
+ const workspace = useTabWorkspaceOptional();
99
+ const [desktop, setDesktop] = useState(false);
100
+ const [loading, setLoading] = useState(true);
101
+ const [installing, setInstalling] = useState(false);
102
+ const [status, setStatus] = useState<HermesStatus | null>(null);
103
+ const [showLogs, setShowLogs] = useState(false);
104
+ const [tunnelHermes, setTunnelHermes] = useState<string | null>(null);
105
+
106
+ const refresh = useCallback(async () => {
107
+ const api = getShellDesktopApi();
108
+ if (!api?.getHermesStatus) return;
109
+ setLoading(true);
110
+ try {
111
+ setStatus((await api.getHermesStatus()) as HermesStatus);
112
+ if (api.getTunnelStatus) {
113
+ const t = (await api.getTunnelStatus()) as {
114
+ publicUrls?: { hermes?: string | null };
115
+ };
116
+ setTunnelHermes(t.publicUrls?.hermes || null);
117
+ }
118
+ } finally {
119
+ setLoading(false);
120
+ }
121
+ }, []);
122
+
123
+ useEffect(() => {
124
+ const api = getShellDesktopApi();
125
+ if (!api?.getHermesStatus) return;
126
+ setDesktop(true);
127
+ void refresh();
128
+ }, [refresh]);
129
+
130
+ useEffect(() => {
131
+ if (!status?.installing && status?.status !== "starting" && !installing) return;
132
+ const t = setInterval(() => {
133
+ void refresh();
134
+ }, 2500);
135
+ return () => clearInterval(t);
136
+ }, [status?.installing, status?.status, installing, refresh]);
137
+
138
+ // Web (serveur headless) : statut via /api/v1/os/hermes/status.
139
+ if (!desktop) return <ServerServiceStatusCard kind="hermes" />;
140
+
141
+ async function onInstallRuntime() {
142
+ const api = getShellDesktopApi();
143
+ if (!api?.ensureHermesRuntime) {
144
+ toast.error("API ensureHermesRuntime indisponible — recompilez Electron.");
145
+ return;
146
+ }
147
+ setInstalling(true);
148
+ try {
149
+ const r = await api.ensureHermesRuntime();
150
+ if (r.ok) {
151
+ const restarted = api.retryHermes ? await api.retryHermes() : null;
152
+ if (restarted?.webuiUrl) {
153
+ toast.success("Hermes réparé et relancé.");
154
+ } else {
155
+ toast.error(
156
+ restarted?.detail ||
157
+ "Runtime réparé, mais Hermes ne répond pas. Consultez les logs.",
158
+ );
159
+ }
160
+ } else {
161
+ toast.error(r.detail || "Échec installation runtime");
162
+ }
163
+ await refresh();
164
+ } catch (e) {
165
+ toast.error(e instanceof Error ? e.message : "Erreur bootstrap");
166
+ } finally {
167
+ setInstalling(false);
168
+ }
169
+ }
170
+
171
+ async function onOpenWebuiTab() {
172
+ let retryToast: string | number | undefined;
173
+ const target = await resolveHermesWebuiOpenTarget({
174
+ onRetry: () => {
175
+ retryToast = toast.loading("Relance de l’API et de la WebUI Hermes…");
176
+ },
177
+ });
178
+ if (retryToast !== undefined) toast.dismiss(retryToast);
179
+ if (!isHermesWebuiOpenTarget(target)) {
180
+ toast.error(target.error);
181
+ return;
182
+ }
183
+ if (workspace?.openExternalSite) {
184
+ workspace.openExternalSite({
185
+ siteId: target.siteId,
186
+ url: target.url,
187
+ title: target.title,
188
+ });
189
+ toast.success("Onglet Hermes ouvert");
190
+ return;
191
+ }
192
+ const api = getShellDesktopApi();
193
+ if (api?.openTab) {
194
+ try {
195
+ await api.openTab(target.siteId, target.url);
196
+ toast.success("Onglet Hermes ouvert");
197
+ return;
198
+ } catch (e) {
199
+ toast.error(e instanceof Error ? e.message : "Ouverture impossible");
200
+ return;
201
+ }
202
+ }
203
+ window.open(target.url, "_blank", "noreferrer");
204
+ }
205
+
206
+ const busy = installing || Boolean(status?.installing);
207
+
208
+ return (
209
+ <Card>
210
+ <CardHeader>
211
+ <CardTitle className="flex items-center gap-2 text-base">
212
+ <Bot className="h-4 w-4" /> Hermes Agent
213
+ </CardTitle>
214
+ <CardDescription>
215
+ Agents natifs Creezio. Workspace et HOME confinés dans l’OS Creezio
216
+ — jamais dans C:\Users\…
217
+ </CardDescription>
218
+ </CardHeader>
219
+ <CardContent className="space-y-4">
220
+ <div
221
+ className={`rounded-md border px-3 py-2 text-sm ${statusTone(status?.status || "")}`}
222
+ >
223
+ {loading || !status ? (
224
+ <span className="inline-flex items-center gap-2">
225
+ <Loader2 className="h-3.5 w-3.5 animate-spin" /> Chargement…
226
+ </span>
227
+ ) : (
228
+ <>
229
+ <div className="font-medium">
230
+ {statusLabel(status.status)}
231
+ {status.version ? ` · v${status.version}` : ""}
232
+ </div>
233
+ <div className="mt-0.5 text-xs opacity-90">{status.detail}</div>
234
+ <div className="mt-1 text-xs opacity-90">
235
+ {webuiLabel(status.webuiStatus)}
236
+ </div>
237
+ {status.bootstrapError ? (
238
+ <div className="mt-1 text-xs text-amber-800">{status.bootstrapError}</div>
239
+ ) : null}
240
+ </>
241
+ )}
242
+ </div>
243
+
244
+ <div>
245
+ <div className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
246
+ Service (pré-configuré)
247
+ </div>
248
+ <div className="grid gap-2 sm:grid-cols-2">
249
+ <LockedConfigField
250
+ label="Mode"
251
+ value="Embarqué (natif Creezio)"
252
+ />
253
+ <LockedConfigField
254
+ label="home_mode"
255
+ value="profile"
256
+ hint="HOME outils = hermes-home/home (sandbox OS)."
257
+ />
258
+ <LockedConfigField
259
+ label="Workspace agent"
260
+ value={
261
+ status?.homeDir
262
+ ? `${status.homeDir.replace(/[/\\]+$/, "")}/workspace`
263
+ : "…/hermes-home/workspace"
264
+ }
265
+ hint="Seul répertoire de travail autorisé."
266
+ />
267
+ <LockedConfigField
268
+ label="HERMES_HOME"
269
+ value={status?.homeDir || "…/hermes-home"}
270
+ />
271
+ <LockedConfigField
272
+ label="API locale"
273
+ value={status?.apiUrl || "http://127.0.0.1:18642"}
274
+ />
275
+ <LockedConfigField
276
+ label="WebUI locale"
277
+ value={status?.webuiUrl || "http://127.0.0.1:18797"}
278
+ />
279
+ <LockedConfigField
280
+ label="URL publique tunnel"
281
+ value={tunnelHermes || "Tunnel non réservé"}
282
+ />
283
+ <LockedConfigField
284
+ label="GATEWAY_ALLOW_ALL_USERS"
285
+ value="true"
286
+ hint="Desktop local — messagerie multi-user non utilisée."
287
+ />
288
+ </div>
289
+ </div>
290
+
291
+ <DesktopEmbedEnvPanel service="hermes" />
292
+
293
+ <div className="flex flex-wrap gap-2">
294
+ <Button
295
+ size="sm"
296
+ variant="secondary"
297
+ onClick={() => void onOpenWebuiTab()}
298
+ disabled={busy}
299
+ >
300
+ <ExternalLink className="mr-1 h-3.5 w-3.5" />
301
+ Ouvrir dans un onglet
302
+ </Button>
303
+ <Button
304
+ size="sm"
305
+ variant="outline"
306
+ onClick={() => void onInstallRuntime()}
307
+ disabled={busy || status?.status === "skipped-remote-client"}
308
+ >
309
+ {installing || status?.installing ? (
310
+ <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
311
+ ) : (
312
+ <Download className="mr-1 h-3.5 w-3.5" />
313
+ )}
314
+ Réparer
315
+ </Button>
316
+ <Button
317
+ size="sm"
318
+ variant="outline"
319
+ onClick={() => void refresh()}
320
+ disabled={loading}
321
+ >
322
+ <RefreshCw className="mr-1 h-3.5 w-3.5" /> Actualiser
323
+ </Button>
324
+ <Button
325
+ size="sm"
326
+ variant="ghost"
327
+ onClick={() => setShowLogs((v) => !v)}
328
+ >
329
+ {showLogs ? "Masquer les logs" : "Logs"}
330
+ </Button>
331
+ </div>
332
+
333
+ {showLogs && status?.logs?.length ? (
334
+ <pre className="max-h-48 overflow-auto rounded-md border bg-slate-950 p-2 font-mono text-[11px] leading-relaxed text-slate-100">
335
+ {status.logs.join("\n")}
336
+ </pre>
337
+ ) : null}
338
+ </CardContent>
339
+ </Card>
340
+ );
341
+ }