@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,251 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Profil de connexion desktop : héberger un serveur local vs rejoindre un serveur.
7
+ * Changer de profil relance l'application Electron.
8
+ *
9
+ * Bind LAN (0.0.0.0) : option avancée hôte uniquement — pas au boot.
10
+ * Accès distant produit = tunnel Cloudflare (config serveur / onboarding).
11
+ */
12
+
13
+ import { useCallback, useEffect, useState } from "react";
14
+ import { toast } from "sonner";
15
+ import { Loader2, Network, Server } from "lucide-react";
16
+ import { Button } from "../primitives/button";
17
+ import { Input } from "../primitives/input";
18
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
19
+ import { ServerInstanceCard } from "./server-mode-cards";
20
+
21
+ type ConnProfile = {
22
+ mode: "local" | "remote";
23
+ remoteUrl: string | null;
24
+ localBind: "127.0.0.1" | "0.0.0.0";
25
+ chosen: boolean;
26
+ activeBaseUrl: string | null;
27
+ serverPort: number | null;
28
+ };
29
+
30
+ export function DesktopConnectionSettings() {
31
+ const [desktop, setDesktop] = useState(false);
32
+ const [profile, setProfile] = useState<ConnProfile | null>(null);
33
+ const [mode, setMode] = useState<"local" | "remote">("local");
34
+ const [remoteUrl, setRemoteUrl] = useState("");
35
+ const [bindLan, setBindLan] = useState(false);
36
+ const [testing, setTesting] = useState(false);
37
+ const [saving, setSaving] = useState(false);
38
+ const [testMsg, setTestMsg] = useState<string | null>(null);
39
+
40
+ const refresh = useCallback(async () => {
41
+ const api = getShellDesktopApi();
42
+ if (!api?.getConnectionProfile) return;
43
+ const p = await api.getConnectionProfile();
44
+ setProfile(p);
45
+ setMode(p.mode);
46
+ setRemoteUrl(p.remoteUrl || "");
47
+ setBindLan(p.localBind === "0.0.0.0");
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ const api = getShellDesktopApi();
52
+ if (!api?.getConnectionProfile) return;
53
+ setDesktop(true);
54
+ void refresh();
55
+ }, [refresh]);
56
+
57
+ // Web (serveur headless) : pas de profil Héberger/Rejoindre — carte serveur.
58
+ if (!desktop) return <ServerInstanceCard />;
59
+
60
+ async function onTest() {
61
+ const api = getShellDesktopApi();
62
+ if (!api?.testConnection) return;
63
+ setTesting(true);
64
+ setTestMsg(null);
65
+ try {
66
+ const r = await api.testConnection(remoteUrl);
67
+ if (r.ok) {
68
+ setTestMsg(`OK — ${r.baseUrl || remoteUrl}`);
69
+ if (r.baseUrl) setRemoteUrl(r.baseUrl);
70
+ toast.success("Serveur joignable");
71
+ } else {
72
+ setTestMsg(r.error || "Échec");
73
+ toast.error(r.error || "Serveur injoignable");
74
+ }
75
+ } catch (e) {
76
+ const msg = e instanceof Error ? e.message : "Erreur";
77
+ setTestMsg(msg);
78
+ toast.error(msg);
79
+ } finally {
80
+ setTesting(false);
81
+ }
82
+ }
83
+
84
+ async function onApply() {
85
+ const api = getShellDesktopApi();
86
+ if (!api?.applyConnection) return;
87
+ setSaving(true);
88
+ try {
89
+ const r = await api.applyConnection({
90
+ mode,
91
+ remoteUrl: mode === "remote" ? remoteUrl : undefined,
92
+ localBind: mode === "local" ? (bindLan ? "0.0.0.0" : "127.0.0.1") : "127.0.0.1",
93
+ chosen: true,
94
+ });
95
+ if (!r.ok) {
96
+ toast.error(r.error || "Impossible d'appliquer");
97
+ return;
98
+ }
99
+ toast.success("Profil enregistré — redémarrage…");
100
+ } catch (e) {
101
+ toast.error(e instanceof Error ? e.message : "Erreur");
102
+ } finally {
103
+ setSaving(false);
104
+ }
105
+ }
106
+
107
+ const isRemoteSession = profile?.mode === "remote";
108
+
109
+ return (
110
+ <Card>
111
+ <CardHeader className="pb-2">
112
+ <CardTitle className="flex items-center gap-2 text-base">
113
+ <Network className="h-4 w-4" />
114
+ Connexion serveur
115
+ </CardTitle>
116
+ <CardDescription>
117
+ Héberger le serveur sur ce PC (tunnel d&apos;accès distant inclus dans la config
118
+ serveur), ou rejoindre un serveur du cabinet via son URL. Un changement relance
119
+ l&apos;application.
120
+ </CardDescription>
121
+ </CardHeader>
122
+ <CardContent className="space-y-4 text-sm">
123
+ {profile?.activeBaseUrl && (
124
+ <p className="rounded-md border bg-muted/40 px-3 py-2 text-muted-foreground">
125
+ Session actuelle :{" "}
126
+ <span className="font-medium text-foreground">{profile.activeBaseUrl}</span>
127
+ {profile.mode === "local" && profile.serverPort != null && (
128
+ <> (port {profile.serverPort})</>
129
+ )}
130
+ </p>
131
+ )}
132
+
133
+ <div className="grid gap-2 sm:grid-cols-2">
134
+ <button
135
+ type="button"
136
+ onClick={() => setMode("local")}
137
+ className={`rounded-lg border p-3 text-left transition-colors ${
138
+ mode === "local"
139
+ ? "border-sky-500 bg-sky-500/5"
140
+ : "border-border hover:bg-muted/40"
141
+ }`}
142
+ >
143
+ <div className="flex items-center gap-2 font-medium">
144
+ <Server className="h-4 w-4" />
145
+ Héberger (serveur local)
146
+ </div>
147
+ <p className="mt-1 text-xs text-muted-foreground">
148
+ Ce poste démarre la base et l&apos;API. L&apos;accès distant passe par le tunnel
149
+ configuré avec le serveur (carte Tunnel ci-dessous).
150
+ </p>
151
+ </button>
152
+ <button
153
+ type="button"
154
+ onClick={() => setMode("remote")}
155
+ className={`rounded-lg border p-3 text-left transition-colors ${
156
+ mode === "remote"
157
+ ? "border-sky-500 bg-sky-500/5"
158
+ : "border-border hover:bg-muted/40"
159
+ }`}
160
+ >
161
+ <div className="flex items-center gap-2 font-medium">
162
+ <Network className="h-4 w-4" />
163
+ Rejoindre un serveur
164
+ </div>
165
+ <p className="mt-1 text-xs text-muted-foreground">
166
+ Client léger : pas de serveur local — URL tunnel{" "}
167
+ <code className="text-[11px]">https://….creez.io</code> (ou LAN si
168
+ activé côté hôte).
169
+ </p>
170
+ </button>
171
+ </div>
172
+
173
+ {mode === "local" && (
174
+ <details className="rounded-md border px-3 py-2 text-xs text-muted-foreground">
175
+ <summary className="cursor-pointer font-medium text-foreground">
176
+ Configuration avancée (hôte)
177
+ </summary>
178
+ <label className="mt-3 flex items-start gap-2 leading-snug">
179
+ <input
180
+ type="checkbox"
181
+ className="mt-0.5"
182
+ checked={bindLan}
183
+ onChange={(e) => setBindLan(e.target.checked)}
184
+ />
185
+ <span>
186
+ Écouter aussi sur le réseau local (<code className="text-[11px]">0.0.0.0</code>)
187
+ pour un accès HTTP LAN en plus du tunnel. Autoriser le port TCP dans le
188
+ pare-feu Windows (profil Réseau privé). Les clients utilisent{" "}
189
+ <code className="text-[11px]">http://&lt;IP-LAN&gt;:&lt;port&gt;</code>.
190
+ Par défaut produit : accès distant = tunnel uniquement (pas de choix au
191
+ démarrage).
192
+ </span>
193
+ </label>
194
+ </details>
195
+ )}
196
+
197
+ {mode === "remote" && (
198
+ <div className="space-y-2">
199
+ <label className="text-xs text-muted-foreground" htmlFor="remote-url">
200
+ URL du serveur (tunnel ou LAN)
201
+ </label>
202
+ <Input
203
+ id="remote-url"
204
+ value={remoteUrl}
205
+ onChange={(e) => setRemoteUrl(e.target.value)}
206
+ placeholder="https://cabinet.creez.io ou http://192.168.1.10:18790"
207
+ spellCheck={false}
208
+ autoComplete="off"
209
+ />
210
+ {testMsg && (
211
+ <p className="text-xs text-muted-foreground">{testMsg}</p>
212
+ )}
213
+ <Button
214
+ type="button"
215
+ variant="outline"
216
+ size="sm"
217
+ disabled={testing || !remoteUrl.trim()}
218
+ onClick={() => void onTest()}
219
+ >
220
+ {testing ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : null}
221
+ Tester la connexion
222
+ </Button>
223
+ </div>
224
+ )}
225
+
226
+ {isRemoteSession && (
227
+ <p className="text-xs text-amber-700 dark:text-amber-400">
228
+ Mode distant : tunnel, clés IA serveur, réindexation Meili et remise à zéro
229
+ usine concernent uniquement le PC hôte.
230
+ </p>
231
+ )}
232
+
233
+ <Button type="button" disabled={saving} onClick={() => void onApply()}>
234
+ {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
235
+ Enregistrer et redémarrer
236
+ </Button>
237
+ </CardContent>
238
+ </Card>
239
+ );
240
+ }
241
+
242
+ /** Masque les réglages réservés à l'hôte local quand on est en client distant. */
243
+ export function useIsRemoteDesktopClient(): boolean {
244
+ const [remote, setRemote] = useState(false);
245
+ useEffect(() => {
246
+ const api = getShellDesktopApi();
247
+ if (!api?.getConnectionProfile) return;
248
+ void api.getConnectionProfile().then((p: { mode: string }) => setRemote(p.mode === "remote"));
249
+ }, []);
250
+ return remote;
251
+ }
@@ -0,0 +1,257 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Panneau variables d’environnement pour n8n / Hermes.
7
+ * - Locked : visibles, non éditables (OS Creezio)
8
+ * - Editables + custom : persistées via embed-env:set, puis redémarrage du service
9
+ */
10
+
11
+ import { useCallback, useEffect, useState } from "react";
12
+ import { toast } from "sonner";
13
+ import { Loader2, Plus, Save, Trash2 } from "lucide-react";
14
+ import { Button } from "../primitives/button";
15
+ import { Input } from "../primitives/input";
16
+ import { LockedConfigField } from "./locked-config-field";
17
+
18
+ type EmbedService = "n8n" | "hermes";
19
+
20
+ type EnvVar = {
21
+ key: string;
22
+ label: string;
23
+ hint?: string;
24
+ locked: boolean;
25
+ value: string;
26
+ kind?: "string" | "boolean" | "number";
27
+ custom?: boolean;
28
+ };
29
+
30
+ export function DesktopEmbedEnvPanel(props: { service: EmbedService }) {
31
+ const [desktop, setDesktop] = useState(false);
32
+ const [loading, setLoading] = useState(true);
33
+ const [saving, setSaving] = useState(false);
34
+ const [locked, setLocked] = useState<EnvVar[]>([]);
35
+ const [editable, setEditable] = useState<EnvVar[]>([]);
36
+ const [customKey, setCustomKey] = useState("");
37
+ const [customValue, setCustomValue] = useState("");
38
+
39
+ const applyPanel = useCallback((vars: EnvVar[]) => {
40
+ setLocked(vars.filter((v) => v.locked));
41
+ setEditable(
42
+ vars
43
+ .filter((v) => !v.locked)
44
+ .map((v) => ({ ...v, value: v.value ?? "" })),
45
+ );
46
+ }, []);
47
+
48
+ const refresh = useCallback(async () => {
49
+ const api = getShellDesktopApi();
50
+ if (!api?.getEmbedEnv) return;
51
+ setLoading(true);
52
+ try {
53
+ const panel = await api.getEmbedEnv(props.service);
54
+ applyPanel(panel.vars as EnvVar[]);
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ }, [applyPanel, props.service]);
59
+
60
+ useEffect(() => {
61
+ if (!getShellDesktopApi()?.getEmbedEnv) return;
62
+ setDesktop(true);
63
+ void refresh();
64
+ }, [refresh]);
65
+
66
+ function setEditableValue(key: string, value: string) {
67
+ setEditable((prev) =>
68
+ prev.map((v) => (v.key === key ? { ...v, value } : v)),
69
+ );
70
+ }
71
+
72
+ function removeCustom(key: string) {
73
+ setEditable((prev) => prev.filter((v) => !(v.custom && v.key === key)));
74
+ }
75
+
76
+ function addCustom() {
77
+ const key = customKey.trim();
78
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
79
+ toast.error("Nom de variable invalide (A-Z, 0-9, _)");
80
+ return;
81
+ }
82
+ if (locked.some((v) => v.key.toUpperCase() === key.toUpperCase())) {
83
+ toast.error("Cette clé est verrouillée par Creezio");
84
+ return;
85
+ }
86
+ if (editable.some((v) => v.key.toUpperCase() === key.toUpperCase())) {
87
+ toast.error("Variable déjà présente");
88
+ return;
89
+ }
90
+ setEditable((prev) => [
91
+ ...prev,
92
+ {
93
+ key,
94
+ label: key,
95
+ value: customValue,
96
+ locked: false,
97
+ custom: true,
98
+ kind: "string",
99
+ },
100
+ ]);
101
+ setCustomKey("");
102
+ setCustomValue("");
103
+ }
104
+
105
+ async function onSave() {
106
+ const api = getShellDesktopApi();
107
+ if (!api?.setEmbedEnv) return;
108
+ setSaving(true);
109
+ try {
110
+ const values: Record<string, string> = {};
111
+ for (const v of editable) {
112
+ const val = String(v.value ?? "").trim();
113
+ if (val !== "") values[v.key] = val;
114
+ }
115
+ const r = await api.setEmbedEnv(props.service, values);
116
+ applyPanel(r.panel.vars as EnvVar[]);
117
+ toast.success(r.detail || "Enregistré");
118
+ } catch (e) {
119
+ toast.error(e instanceof Error ? e.message : "Erreur enregistrement");
120
+ } finally {
121
+ setSaving(false);
122
+ }
123
+ }
124
+
125
+ if (!desktop) return null;
126
+
127
+ return (
128
+ <div className="space-y-4 border-t border-slate-200 pt-4">
129
+ <div>
130
+ <div className="mb-1 text-xs font-semibold uppercase tracking-wide text-slate-500">
131
+ Variables d’environnement
132
+ </div>
133
+ <p className="text-[11px] leading-snug text-slate-500">
134
+ Les clés OS Creezio (tunnel, ports, sandbox) sont visibles mais
135
+ verrouillées. Les autres sont éditables et appliquées au prochain
136
+ démarrage du service (redémarrage automatique à l’enregistrement).
137
+ </p>
138
+ </div>
139
+
140
+ {loading ? (
141
+ <div className="inline-flex items-center gap-2 text-sm text-slate-600">
142
+ <Loader2 className="h-3.5 w-3.5 animate-spin" /> Chargement des
143
+ variables…
144
+ </div>
145
+ ) : (
146
+ <>
147
+ {locked.length > 0 ? (
148
+ <div>
149
+ <div className="mb-2 text-xs font-medium text-slate-500">
150
+ Verrouillées (OS)
151
+ </div>
152
+ <div className="grid gap-2 sm:grid-cols-2">
153
+ {locked.map((v) => (
154
+ <LockedConfigField
155
+ key={v.key}
156
+ label={`${v.label} (${v.key})`}
157
+ value={v.value || "—"}
158
+ hint={v.hint}
159
+ />
160
+ ))}
161
+ </div>
162
+ </div>
163
+ ) : null}
164
+
165
+ <div>
166
+ <div className="mb-2 text-xs font-medium text-slate-500">
167
+ Éditables
168
+ </div>
169
+ <div className="space-y-2">
170
+ {editable.map((v) => (
171
+ <div
172
+ key={v.key}
173
+ className="rounded-md border border-slate-200 bg-white px-3 py-2"
174
+ >
175
+ <div className="flex items-start justify-between gap-2">
176
+ <div className="min-w-0">
177
+ <div className="text-xs font-medium text-slate-700">
178
+ {v.label}
179
+ <span className="ml-1 font-mono text-[11px] text-slate-400">
180
+ {v.key}
181
+ </span>
182
+ </div>
183
+ {v.hint ? (
184
+ <div className="mt-0.5 text-[11px] text-slate-500">
185
+ {v.hint}
186
+ </div>
187
+ ) : null}
188
+ </div>
189
+ {v.custom ? (
190
+ <Button
191
+ type="button"
192
+ size="sm"
193
+ variant="ghost"
194
+ className="h-7 shrink-0 px-2"
195
+ onClick={() => removeCustom(v.key)}
196
+ aria-label={`Supprimer ${v.key}`}
197
+ >
198
+ <Trash2 className="h-3.5 w-3.5" />
199
+ </Button>
200
+ ) : null}
201
+ </div>
202
+ <Input
203
+ className="mt-1.5 font-mono text-xs"
204
+ value={v.value}
205
+ onChange={(e) => setEditableValue(v.key, e.target.value)}
206
+ placeholder={v.kind === "boolean" ? "true | false" : ""}
207
+ />
208
+ </div>
209
+ ))}
210
+ </div>
211
+ </div>
212
+
213
+ <div className="rounded-md border border-dashed border-slate-300 px-3 py-2">
214
+ <div className="mb-2 text-xs font-medium text-slate-600">
215
+ Ajouter une variable
216
+ </div>
217
+ <div className="flex flex-col gap-2 sm:flex-row">
218
+ <Input
219
+ className="font-mono text-xs sm:max-w-[220px]"
220
+ placeholder="NOM_VARIABLE"
221
+ value={customKey}
222
+ onChange={(e) => setCustomKey(e.target.value)}
223
+ />
224
+ <Input
225
+ className="font-mono text-xs"
226
+ placeholder="valeur"
227
+ value={customValue}
228
+ onChange={(e) => setCustomValue(e.target.value)}
229
+ />
230
+ <Button
231
+ type="button"
232
+ size="sm"
233
+ variant="outline"
234
+ onClick={addCustom}
235
+ >
236
+ <Plus className="mr-1 h-3.5 w-3.5" /> Ajouter
237
+ </Button>
238
+ </div>
239
+ </div>
240
+
241
+ <Button
242
+ size="sm"
243
+ onClick={() => void onSave()}
244
+ disabled={saving}
245
+ >
246
+ {saving ? (
247
+ <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
248
+ ) : (
249
+ <Save className="mr-1 h-3.5 w-3.5" />
250
+ )}
251
+ Enregistrer et redémarrer
252
+ </Button>
253
+ </>
254
+ )}
255
+ </div>
256
+ );
257
+ }