@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,264 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Tunnel d'accès distant via Cloudflare — app desktop (hôte) uniquement.
7
+ *
8
+ * Expose le serveur local en HTTPS (`{slug}.creez.io`) pour :
9
+ * desktop « Rejoindre un serveur », navigateur, mobile / PWA.
10
+ * PC éteint ou tunnel arrêté = URL inaccessible.
11
+ */
12
+
13
+ import { useCallback, useEffect, useState } from "react";
14
+ import { toast } from "sonner";
15
+ import { Copy, Globe, Loader2, Wifi, WifiOff } 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 { ServerTunnelCard } from "./server-mode-cards";
20
+
21
+ type TunnelPublicUrls = {
22
+ crm: string;
23
+ n8n: string;
24
+ hermes: string;
25
+ };
26
+
27
+ type TunnelStatus = {
28
+ configured: boolean;
29
+ online: boolean;
30
+ slug: string | null;
31
+ hostname: string | null;
32
+ publicUrl: string | null;
33
+ publicUrls?: TunnelPublicUrls | null;
34
+ error: string | null;
35
+ pcMustBeOn: true;
36
+ };
37
+
38
+ const SLUG_RE = /^[a-z0-9]([a-z0-9-]{0,46}[a-z0-9])$/;
39
+
40
+ export function DesktopTunnel() {
41
+ const [desktop, setDesktop] = useState(false);
42
+ const [status, setStatus] = useState<TunnelStatus | null>(null);
43
+ const [slug, setSlug] = useState("");
44
+ const [checking, setChecking] = useState(false);
45
+ const [reserving, setReserving] = useState(false);
46
+ const [avail, setAvail] = useState<{ available: boolean; reason?: string } | null>(null);
47
+
48
+ const refresh = useCallback(async () => {
49
+ const api = getShellDesktopApi();
50
+ if (!api?.getTunnelStatus) return;
51
+ setStatus(await api.getTunnelStatus());
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ const api = getShellDesktopApi();
56
+ if (!api?.getTunnelStatus) return;
57
+ setDesktop(true);
58
+ void refresh();
59
+ const t = setInterval(() => void refresh(), 5000);
60
+ return () => clearInterval(t);
61
+ }, [refresh]);
62
+
63
+ // Web (serveur headless) : URLs publiques via /api/v1/os/tunnel/status.
64
+ if (!desktop) return <ServerTunnelCard />;
65
+
66
+ async function checkAvailability() {
67
+ const api = getShellDesktopApi();
68
+ if (!api?.checkTunnelSlug) return;
69
+ const s = slug.trim().toLowerCase();
70
+ if (!SLUG_RE.test(s)) {
71
+ setAvail({ available: false, reason: "Slug invalide (a-z, 0-9, tirets, 2–48 car.)" });
72
+ return;
73
+ }
74
+ setChecking(true);
75
+ setAvail(null);
76
+ try {
77
+ const r = await api.checkTunnelSlug(s);
78
+ setAvail(r);
79
+ } catch (e) {
80
+ setAvail({ available: false, reason: e instanceof Error ? e.message : "Erreur réseau" });
81
+ } finally {
82
+ setChecking(false);
83
+ }
84
+ }
85
+
86
+ async function reserve() {
87
+ const api = getShellDesktopApi();
88
+ if (!api?.reserveTunnel) return;
89
+ const s = slug.trim().toLowerCase();
90
+ if (!SLUG_RE.test(s)) {
91
+ toast.error("Slug invalide");
92
+ return;
93
+ }
94
+ setReserving(true);
95
+ try {
96
+ const r = await api.reserveTunnel(s);
97
+ if (!r.ok) {
98
+ toast.error(r.error || "Réservation impossible");
99
+ return;
100
+ }
101
+ toast.success(`Réservé : ${r.hostname}`);
102
+ setSlug("");
103
+ setAvail(null);
104
+ await refresh();
105
+ } catch (e) {
106
+ toast.error(e instanceof Error ? e.message : "Erreur de réservation");
107
+ } finally {
108
+ setReserving(false);
109
+ }
110
+ }
111
+
112
+ async function stopTunnel() {
113
+ const api = getShellDesktopApi();
114
+ if (!api?.stopTunnel) return;
115
+ try {
116
+ await api.stopTunnel();
117
+ toast.message("Tunnel arrêté (URL d’accès distant inaccessible)");
118
+ await refresh();
119
+ } catch (e) {
120
+ toast.error(e instanceof Error ? e.message : "Erreur");
121
+ }
122
+ }
123
+
124
+ async function startTunnel() {
125
+ const api = getShellDesktopApi();
126
+ if (!api?.startTunnel) return;
127
+ try {
128
+ await api.startTunnel();
129
+ toast.success("Tunnel d’accès distant démarré");
130
+ await refresh();
131
+ } catch (e) {
132
+ toast.error(e instanceof Error ? e.message : "Erreur de démarrage");
133
+ }
134
+ }
135
+
136
+ async function copyPublicUrl() {
137
+ const url = status?.publicUrl;
138
+ if (!url) return;
139
+ try {
140
+ await navigator.clipboard.writeText(url);
141
+ toast.success("URL copiée — à coller dans « Rejoindre un serveur » sur les autres PC");
142
+ } catch {
143
+ toast.error("Impossible de copier l’URL");
144
+ }
145
+ }
146
+
147
+ const online = status?.online ?? false;
148
+ const configured = status?.configured ?? false;
149
+
150
+ return (
151
+ <Card className="mt-4">
152
+ <CardHeader>
153
+ <CardTitle className="flex items-center gap-2 text-base">
154
+ <Globe className="h-4 w-4" /> Tunnel d&apos;accès distant
155
+ </CardTitle>
156
+ <CardDescription>
157
+ Expose ce serveur en HTTPS :{" "}
158
+ <code className="text-xs">{"{slug}.creez.io"}</code> (CRM) et{" "}
159
+ <code className="text-xs">{"n8n|hermes.{slug}.creez.io"}</code>{" "}
160
+ (embeds). Même URL CRM pour « Rejoindre un serveur », navigateur et mobile /
161
+ PWA. Ce PC doit rester allumé avec le tunnel en ligne.
162
+ </CardDescription>
163
+ </CardHeader>
164
+ <CardContent className="space-y-3">
165
+ <p
166
+ className={
167
+ online
168
+ ? "flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800"
169
+ : "flex items-center gap-2 rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700"
170
+ }
171
+ >
172
+ {online ? <Wifi className="h-4 w-4" /> : <WifiOff className="h-4 w-4" />}
173
+ {configured
174
+ ? online
175
+ ? `En ligne — ${status?.publicUrl}`
176
+ : `Hors ligne — ${status?.hostname || "tunnel configuré"} (PC éteint ou tunnel arrêté)`
177
+ : "Aucun sous-domaine réservé"}
178
+ </p>
179
+ {configured && status?.publicUrl ? (
180
+ <div className="space-y-1.5 text-xs text-muted-foreground">
181
+ <p>
182
+ Sur les autres postes : Configuration → Connexion serveur →{" "}
183
+ <strong className="font-medium text-foreground">Rejoindre un serveur</strong>,
184
+ coller l&apos;URL CRM (ou au premier lancement choisir « Rejoindre »). Les
185
+ embeds s&apos;ouvrent automatiquement via leurs sous-domaines.
186
+ </p>
187
+ {status.publicUrls ? (
188
+ <ul className="space-y-0.5 font-mono text-[11px] text-foreground/80">
189
+ <li>CRM · {status.publicUrls.crm}</li>
190
+ <li>n8n · {status.publicUrls.n8n}</li>
191
+ <li>Hermes · {status.publicUrls.hermes}</li>
192
+ </ul>
193
+ ) : null}
194
+ </div>
195
+ ) : null}
196
+ {status?.error ? (
197
+ <p className="text-sm text-destructive">{status.error}</p>
198
+ ) : null}
199
+
200
+ {!configured ? (
201
+ <div className="space-y-2">
202
+ <div className="flex items-center gap-2">
203
+ <Input
204
+ value={slug}
205
+ onChange={(e) => {
206
+ setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""));
207
+ setAvail(null);
208
+ }}
209
+ placeholder="mon-cabinet"
210
+ className="flex-1"
211
+ maxLength={48}
212
+ />
213
+ <span className="shrink-0 text-sm text-muted-foreground">.creez.io</span>
214
+ </div>
215
+ <div className="flex flex-wrap gap-2">
216
+ <Button size="sm" variant="outline" disabled={checking || !slug} onClick={() => void checkAvailability()}>
217
+ {checking ? <Loader2 className="h-4 w-4 animate-spin" /> : "Vérifier dispo"}
218
+ </Button>
219
+ <Button
220
+ size="sm"
221
+ disabled={reserving || !avail?.available}
222
+ onClick={() => void reserve()}
223
+ >
224
+ {reserving ? <Loader2 className="h-4 w-4 animate-spin" /> : "Réserver & activer"}
225
+ </Button>
226
+ </div>
227
+ {avail ? (
228
+ <p className={avail.available ? "text-sm text-emerald-700" : "text-sm text-amber-800"}>
229
+ {avail.available
230
+ ? `Disponible : ${slug}.creez.io`
231
+ : avail.reason || "Indisponible"}
232
+ </p>
233
+ ) : null}
234
+ </div>
235
+ ) : (
236
+ <div className="flex flex-wrap gap-2">
237
+ {online ? (
238
+ <Button size="sm" variant="outline" onClick={() => void stopTunnel()}>
239
+ Arrêter le tunnel
240
+ </Button>
241
+ ) : (
242
+ <Button size="sm" onClick={() => void startTunnel()}>
243
+ Démarrer le tunnel
244
+ </Button>
245
+ )}
246
+ {status?.publicUrl ? (
247
+ <>
248
+ <Button size="sm" variant="outline" onClick={() => void copyPublicUrl()}>
249
+ <Copy className="mr-1.5 h-3.5 w-3.5" />
250
+ Copier l&apos;URL
251
+ </Button>
252
+ <Button size="sm" variant="outline" asChild>
253
+ <a href={status.publicUrl} target="_blank" rel="noreferrer">
254
+ Ouvrir l&apos;URL
255
+ </a>
256
+ </Button>
257
+ </>
258
+ ) : null}
259
+ </div>
260
+ )}
261
+ </CardContent>
262
+ </Card>
263
+ );
264
+ }
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Mise à jour de l'app desktop — Configuration.
7
+ * Check auto côté main ; téléchargement uniquement après clic.
8
+ */
9
+
10
+ import { useCallback, useEffect, useState } from "react";
11
+ import { toast } from "sonner";
12
+ import { ArrowUpCircle, Loader2, RefreshCw } from "lucide-react";
13
+ import { Button } from "../primitives/button";
14
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
15
+ import type { DesktopUpdateStatus } from "../desktop-types";
16
+ import { ServerVersionCard } from "./server-mode-cards";
17
+
18
+ function labelFor(status: DesktopUpdateStatus | null): string {
19
+ if (!status) return "…";
20
+ switch (status.state) {
21
+ case "disabled":
22
+ return status.error || "Auto-update désactivé";
23
+ case "checking":
24
+ return "Recherche d'une mise à jour…";
25
+ case "available":
26
+ return `Mise à jour disponible : ${status.availableVersion ?? "?"}`;
27
+ case "not-available":
28
+ return "Vous êtes à jour";
29
+ case "downloading":
30
+ return `Téléchargement… ${Math.floor(status.percent ?? 0)} %`;
31
+ case "ready":
32
+ return `Prêt à installer (${status.availableVersion ?? "?"}) — redémarrage…`;
33
+ case "error":
34
+ return status.error || "Erreur de mise à jour";
35
+ case "idle":
36
+ default:
37
+ return status.updateAvailable
38
+ ? `Mise à jour disponible : ${status.availableVersion ?? "?"}`
39
+ : "Aucune vérification récente";
40
+ }
41
+ }
42
+
43
+ export function DesktopUpdateSettings() {
44
+ const [desktop, setDesktop] = useState(false);
45
+ const [status, setStatus] = useState<DesktopUpdateStatus | null>(null);
46
+ const [busy, setBusy] = useState(false);
47
+
48
+ const refresh = useCallback(async () => {
49
+ const api = getShellDesktopApi();
50
+ if (!api?.getUpdateStatus) return;
51
+ setStatus(await api.getUpdateStatus());
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ const api = getShellDesktopApi();
56
+ if (!api?.getUpdateStatus) return;
57
+ setDesktop(true);
58
+ void refresh();
59
+ const unsub = api.onUpdateChanged?.(setStatus);
60
+ return () => unsub?.();
61
+ }, [refresh]);
62
+
63
+ // Web (serveur headless) : version d'image — updates gérées par la flotte.
64
+ if (!desktop) return <ServerVersionCard />;
65
+
66
+ const canInstall =
67
+ status?.state === "available" ||
68
+ status?.state === "ready" ||
69
+ (status?.updateAvailable && status.state !== "downloading");
70
+ const downloading = status?.state === "downloading";
71
+
72
+ async function onCheck() {
73
+ const api = getShellDesktopApi();
74
+ if (!api?.checkForUpdates) return;
75
+ setBusy(true);
76
+ try {
77
+ const s = await api.checkForUpdates();
78
+ setStatus(s);
79
+ if (s.state === "not-available") toast.success("Vous êtes à jour");
80
+ else if (s.state === "available") {
81
+ toast.message(`Mise à jour ${s.availableVersion} disponible`, {
82
+ description: "Cliquez sur « Mettre à jour » pour l'installer.",
83
+ });
84
+ } else if (s.state === "error") toast.error(s.error || "Erreur");
85
+ } catch (e) {
86
+ toast.error(e instanceof Error ? e.message : "Erreur de vérification");
87
+ } finally {
88
+ setBusy(false);
89
+ }
90
+ }
91
+
92
+ async function onInstall() {
93
+ const api = getShellDesktopApi();
94
+ if (!api?.downloadAndInstallUpdate) return;
95
+ setBusy(true);
96
+ try {
97
+ toast.message("Téléchargement de la mise à jour…", {
98
+ description: "L'application redémarrera pour terminer l'installation.",
99
+ });
100
+ const s = await api.downloadAndInstallUpdate();
101
+ setStatus(s);
102
+ if (s.state === "error") toast.error(s.error || "Échec du téléchargement");
103
+ } catch (e) {
104
+ toast.error(e instanceof Error ? e.message : "Échec de la mise à jour");
105
+ } finally {
106
+ setBusy(false);
107
+ }
108
+ }
109
+
110
+ return (
111
+ <Card>
112
+ <CardHeader className="pb-3">
113
+ <CardTitle className="flex items-center gap-2 text-base">
114
+ <ArrowUpCircle className="h-4 w-4" />
115
+ Mises à jour
116
+ </CardTitle>
117
+ <CardDescription>
118
+ Version installée {status?.currentVersion ?? "…"}. Les mises à jour
119
+ ne se téléchargent qu&apos;après votre accord.
120
+ </CardDescription>
121
+ </CardHeader>
122
+ <CardContent className="space-y-3">
123
+ <p
124
+ className={
125
+ status?.state === "error"
126
+ ? "text-sm text-red-600"
127
+ : status?.updateAvailable || status?.state === "available"
128
+ ? "text-sm font-medium text-amber-700"
129
+ : "text-sm text-muted-foreground"
130
+ }
131
+ >
132
+ {labelFor(status)}
133
+ </p>
134
+ {downloading ? (
135
+ <div className="h-2 w-full overflow-hidden rounded bg-muted">
136
+ <div
137
+ className="h-full bg-amber-500 transition-all"
138
+ style={{ width: `${Math.min(100, Math.max(0, status?.percent ?? 0))}%` }}
139
+ />
140
+ </div>
141
+ ) : null}
142
+ <div className="flex flex-wrap gap-2">
143
+ <Button
144
+ type="button"
145
+ variant="outline"
146
+ size="sm"
147
+ disabled={busy || downloading}
148
+ onClick={() => void onCheck()}
149
+ >
150
+ {busy && !downloading ? (
151
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
152
+ ) : (
153
+ <RefreshCw className="mr-2 h-4 w-4" />
154
+ )}
155
+ Vérifier
156
+ </Button>
157
+ <Button
158
+ type="button"
159
+ size="sm"
160
+ disabled={!canInstall || busy || downloading || status?.state === "disabled"}
161
+ onClick={() => void onInstall()}
162
+ >
163
+ {downloading ? (
164
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
165
+ ) : (
166
+ <ArrowUpCircle className="mr-2 h-4 w-4" />
167
+ )}
168
+ Mettre à jour
169
+ {status?.availableVersion ? ` (${status.availableVersion})` : ""}
170
+ </Button>
171
+ </div>
172
+ </CardContent>
173
+ </Card>
174
+ );
175
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Remise à zéro usine — Configuration desktop.
7
+ * Efface compte, DB, Meili, uploads, tunnel token, cookies CRM + sites externes.
8
+ */
9
+
10
+ import { FormEvent, useEffect, useState } from "react";
11
+ import { AlertTriangle, Loader2 } from "lucide-react";
12
+ import { toast } from "sonner";
13
+ import { Button } from "../primitives/button";
14
+ import { Input } from "../primitives/input";
15
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../primitives/card";
16
+ import { ServerOperatorNotice } from "./server-mode-cards";
17
+
18
+ export function FactoryResetSettings() {
19
+ const [confirm, setConfirm] = useState("");
20
+ const [busy, setBusy] = useState(false);
21
+ const [desktop, setDesktop] = useState(false);
22
+
23
+ useEffect(() => {
24
+ setDesktop(Boolean(getShellDesktopApi()?.factoryReset));
25
+ }, []);
26
+
27
+ // Web (serveur headless) : wipe = opération opérateur (backup + recreate).
28
+ if (!desktop)
29
+ return <ServerOperatorNotice label="la remise à zéro usine" />;
30
+
31
+ async function onSubmit(e: FormEvent) {
32
+ e.preventDefault();
33
+ if (confirm.trim().toUpperCase() !== "SUPPRIMER") {
34
+ toast.error('Tapez SUPPRIMER pour confirmer');
35
+ return;
36
+ }
37
+ const api = getShellDesktopApi();
38
+ if (!api?.factoryReset) return;
39
+ setBusy(true);
40
+ try {
41
+ const r = await api.factoryReset();
42
+ if (!r.ok) {
43
+ toast.error(r.error || "Échec de la remise à zéro");
44
+ setBusy(false);
45
+ return;
46
+ }
47
+ toast.success("Remise à zéro en cours…");
48
+ } catch (err) {
49
+ toast.error(err instanceof Error ? err.message : "Erreur");
50
+ setBusy(false);
51
+ }
52
+ }
53
+
54
+ return (
55
+ <Card className="border-rose-200">
56
+ <CardHeader>
57
+ <CardTitle className="flex items-center gap-2 text-base text-rose-700">
58
+ <AlertTriangle className="h-4 w-4" /> Remise à zéro usine
59
+ </CardTitle>
60
+ <CardDescription>
61
+ Efface toutes les données locales de cet ordinateur (compte, base catalogue/métier,
62
+ index Meili, uploads, token tunnel, cookies CRM et sessions sites externes), puis relance
63
+ le wizard de premier lancement. Le DNS Cloudflare distant peut rester — vous
64
+ re-réservez un slug au setup.
65
+ </CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <form onSubmit={(e) => void onSubmit(e)} className="space-y-3">
69
+ <Input
70
+ type="text"
71
+ placeholder='Tapez SUPPRIMER'
72
+ value={confirm}
73
+ onChange={(e) => setConfirm(e.target.value)}
74
+ disabled={busy}
75
+ />
76
+ <Button type="submit" variant="destructive" disabled={busy}>
77
+ {busy ? (
78
+ <>
79
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Effacement…
80
+ </>
81
+ ) : (
82
+ "Tout effacer et repartir de zéro"
83
+ )}
84
+ </Button>
85
+ </form>
86
+ </CardContent>
87
+ </Card>
88
+ );
89
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Affiche les enfants uniquement en mode serveur local (hôte).
5
+ * En client distant, tunnel / BYOK / reindex / factory-reset sont gérés par
6
+ * l'app Serveur : on affiche un encart explicatif (ou rien) à la place.
7
+ */
8
+
9
+ import { useEffect, useState, type ReactNode } from "react";
10
+ import { ServerCog } from "lucide-react";
11
+ import { getShellUiBrand } from "@creezio/shell-ui";
12
+ import { getDesktopHostInfo } from "../lib/desktop-host";
13
+
14
+ /** Encart standard « géré par l'app Serveur » (fallback par défaut). */
15
+ export function HostManagedNotice({
16
+ label = "cette section",
17
+ }: {
18
+ label?: string;
19
+ }) {
20
+ const productName = getShellUiBrand().productName;
21
+ return (
22
+ <div className="flex items-start gap-3 rounded-lg border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600">
23
+ <ServerCog className="mt-0.5 h-5 w-5 shrink-0 text-slate-400" />
24
+ <div>
25
+ <p className="font-medium text-slate-700">
26
+ Géré par l&apos;app {productName} Server
27
+ </p>
28
+ <p className="mt-1">
29
+ Vous êtes connecté à un serveur distant : {label} se configure sur le
30
+ poste qui héberge le serveur, pas depuis ce client.
31
+ </p>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export function HostOnlySettings({
38
+ children,
39
+ fallback,
40
+ }: {
41
+ children: ReactNode;
42
+ /** Affiché à la place des enfants sur un client distant (défaut : encart). */
43
+ fallback?: ReactNode;
44
+ }) {
45
+ // null = résolution en cours : ne rien rendre (pas de flash des contrôles
46
+ // host-only avant le masquage, cause de rejets IPC au montage).
47
+ const [host, setHost] = useState<boolean | null>(null);
48
+
49
+ useEffect(() => {
50
+ let alive = true;
51
+ void getDesktopHostInfo().then((info) => {
52
+ if (alive) setHost(info.isHost);
53
+ });
54
+ return () => {
55
+ alive = false;
56
+ };
57
+ }, []);
58
+
59
+ if (host === null) return null;
60
+ if (!host) return <>{fallback ?? <HostManagedNotice />}</>;
61
+ return <>{children}</>;
62
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Champ de configuration visible mais non modifiable (OS Creezio pré-configuré).
5
+ */
6
+
7
+ import { Lock } from "lucide-react";
8
+
9
+ export function LockedConfigField(props: {
10
+ label: string;
11
+ value: string;
12
+ hint?: string;
13
+ }) {
14
+ return (
15
+ <div className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2">
16
+ <div className="flex items-center gap-1.5 text-xs font-medium text-slate-600">
17
+ <Lock className="h-3 w-3 shrink-0" />
18
+ {props.label}
19
+ </div>
20
+ <div className="mt-1 break-all font-mono text-xs text-slate-900">
21
+ {props.value || "—"}
22
+ </div>
23
+ {props.hint ? (
24
+ <div className="mt-1 text-[11px] leading-snug text-slate-500">
25
+ {props.hint}
26
+ </div>
27
+ ) : null}
28
+ </div>
29
+ );
30
+ }