@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,349 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Section « Clés API » des Paramètres — création / révocation des clés
5
+ * publiques product_live_... (Zapier / Make / n8n). La clé complète n'est
6
+ * montrée qu'une fois à la création ; ensuite seul le prefix est visible.
7
+ */
8
+ import { useEffect, useState } from "react";
9
+ import Link from "next/link";
10
+ import { toast } from "sonner";
11
+ import { BookOpen, Check, Copy, KeyRound, Plus, Trash2 } from "lucide-react";
12
+ import { Badge } from "../primitives/badge";
13
+ import { Button } from "../primitives/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from "../primitives/card";
21
+
22
+ export type ApiKeyItem = {
23
+ id: number;
24
+ name: string;
25
+ prefix: string;
26
+ scopes: string;
27
+ user_id?: string | null;
28
+ created_at: string;
29
+ last_used_at: string | null;
30
+ revoked_at: string | null;
31
+ };
32
+
33
+ export type LinkableUser = { id: string; username: string; kind: "human" | "ai" };
34
+
35
+ function formatDate(value: string | null): string {
36
+ if (!value) return "jamais";
37
+ const d = new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`);
38
+ if (Number.isNaN(d.getTime())) return value;
39
+ return d.toLocaleString("fr-FR", { dateStyle: "medium", timeStyle: "short" });
40
+ }
41
+
42
+ const inputCls =
43
+ "w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-100";
44
+
45
+ export function ApiKeysSettings({
46
+ initialKeys,
47
+ linkableUsers,
48
+ /** @deprecated → linkableUsers (compat Fidu/TF page props). */
49
+ users,
50
+ }: {
51
+ /** Si omis, charge GET /api/v1/api-keys au mount (page OS client). */
52
+ initialKeys?: ApiKeyItem[];
53
+ linkableUsers?: LinkableUser[];
54
+ users?: Array<{ id: string; username: string; role?: string; kind?: "human" | "ai" }>;
55
+ }) {
56
+ const [resolvedUsers, setResolvedUsers] = useState<LinkableUser[]>(
57
+ () =>
58
+ linkableUsers ??
59
+ (users || []).map((u) => ({
60
+ id: u.id,
61
+ username: u.username,
62
+ kind: u.kind ?? (u.role === "ai" || u.role === "agent" ? "ai" : "human"),
63
+ })),
64
+ );
65
+ const [keys, setKeys] = useState<ApiKeyItem[]>(initialKeys ?? []);
66
+ const [name, setName] = useState("");
67
+ const [scope, setScope] = useState("full");
68
+ const [linkedUser, setLinkedUser] = useState("");
69
+ const [busy, setBusy] = useState(false);
70
+ const [freshKey, setFreshKey] = useState<string | null>(null);
71
+ const [copied, setCopied] = useState(false);
72
+
73
+ useEffect(() => {
74
+ if (initialKeys) {
75
+ setKeys(initialKeys);
76
+ return;
77
+ }
78
+ let cancelled = false;
79
+ void fetch("/api/v1/api-keys")
80
+ .then((r) => (r.ok ? r.json() : null))
81
+ .then((data: { api_keys?: ApiKeyItem[] } | null) => {
82
+ if (!cancelled && Array.isArray(data?.api_keys)) setKeys(data.api_keys);
83
+ })
84
+ .catch(() => {
85
+ /* liste vide tant que l'API est indisponible */
86
+ });
87
+ return () => {
88
+ cancelled = true;
89
+ };
90
+ }, [initialKeys]);
91
+
92
+ useEffect(() => {
93
+ if (linkableUsers || users) {
94
+ setResolvedUsers(
95
+ linkableUsers ??
96
+ (users || []).map((u) => ({
97
+ id: u.id,
98
+ username: u.username,
99
+ kind: u.kind ?? (u.role === "ai" || u.role === "agent" ? "ai" : "human"),
100
+ })),
101
+ );
102
+ return;
103
+ }
104
+ let cancelled = false;
105
+ void fetch("/api/v1/users")
106
+ .then((r) => (r.ok ? r.json() : null))
107
+ .then((data: { users?: Array<{ id: string; username: string; kind?: string; active?: boolean }> } | null) => {
108
+ if (cancelled || !Array.isArray(data?.users)) return;
109
+ setResolvedUsers(
110
+ data.users
111
+ .filter((u) => u.active !== false)
112
+ .map((u) => ({
113
+ id: u.id,
114
+ username: u.username,
115
+ kind: u.kind === "ai" ? "ai" : "human",
116
+ })),
117
+ );
118
+ })
119
+ .catch(() => {
120
+ /* owner-only endpoint ; hors droits → liste vide */
121
+ });
122
+ return () => {
123
+ cancelled = true;
124
+ };
125
+ }, [linkableUsers, users]);
126
+
127
+ const requiresUser = scope === "tasks:run";
128
+
129
+ async function createKey() {
130
+ const trimmed = name.trim();
131
+ if (!trimmed) {
132
+ toast.message("Donnez un nom à la clé (ex. « Zapier production »)");
133
+ return;
134
+ }
135
+ if (requiresUser && !linkedUser) {
136
+ toast.message("Le scope « Tâches IA » exige un utilisateur lié");
137
+ return;
138
+ }
139
+ setBusy(true);
140
+ try {
141
+ const res = await fetch("/api/v1/api-keys", {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({
145
+ name: trimmed,
146
+ scopes: scope,
147
+ user_id: linkedUser || null,
148
+ }),
149
+ });
150
+ const data = await res.json().catch(() => ({}));
151
+ if (!res.ok) throw new Error((data as { error?: string }).error || `Erreur ${res.status}`);
152
+ const created = data as { key: string; api_key: ApiKeyItem };
153
+ setKeys((prev) => [created.api_key, ...prev]);
154
+ setFreshKey(created.key);
155
+ setCopied(false);
156
+ setName("");
157
+ setScope("full");
158
+ setLinkedUser("");
159
+ toast.success("Clé API créée — copiez-la maintenant, elle ne sera plus affichée");
160
+ } catch (e) {
161
+ toast.error(e instanceof Error ? e.message : "Création impossible");
162
+ } finally {
163
+ setBusy(false);
164
+ }
165
+ }
166
+
167
+ async function revokeKey(id: number) {
168
+ if (!window.confirm("Révoquer cette clé ? Les intégrations qui l'utilisent cesseront de fonctionner.")) {
169
+ return;
170
+ }
171
+ try {
172
+ const res = await fetch(`/api/v1/api-keys/${id}`, { method: "DELETE" });
173
+ const data = await res.json().catch(() => ({}));
174
+ if (!res.ok) throw new Error((data as { error?: string }).error || `Erreur ${res.status}`);
175
+ setKeys((prev) =>
176
+ prev.map((k) =>
177
+ k.id === id ? { ...k, revoked_at: new Date().toISOString() } : k,
178
+ ),
179
+ );
180
+ toast.success("Clé révoquée");
181
+ } catch (e) {
182
+ toast.error(e instanceof Error ? e.message : "Révocation impossible");
183
+ }
184
+ }
185
+
186
+ async function copyFreshKey() {
187
+ if (!freshKey) return;
188
+ try {
189
+ await navigator.clipboard.writeText(freshKey);
190
+ setCopied(true);
191
+ toast.success("Clé copiée dans le presse-papiers");
192
+ } catch {
193
+ toast.error("Copie impossible — sélectionnez la clé manuellement");
194
+ }
195
+ }
196
+
197
+ return (
198
+ <Card>
199
+ <CardHeader className="pb-4">
200
+ <CardTitle className="flex items-center gap-2 text-base">
201
+ <KeyRound className="h-4 w-4 text-sky-600" />
202
+ Clés API
203
+ </CardTitle>
204
+ <CardDescription>
205
+ Automatisez Creezio avec Zapier, Make ou n8n. Chaque clé donne un
206
+ accès complet à l&apos;API (120 requêtes/minute).{" "}
207
+ <Link
208
+ href="/developers"
209
+ className="inline-flex items-center gap-1 font-medium text-sky-700 underline underline-offset-2 hover:text-sky-900"
210
+ >
211
+ <BookOpen className="h-3.5 w-3.5" />
212
+ Documentation API
213
+ </Link>
214
+ </CardDescription>
215
+ </CardHeader>
216
+ <CardContent className="space-y-5">
217
+ {freshKey ? (
218
+ <div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
219
+ <p className="text-sm font-medium text-emerald-900">
220
+ Votre nouvelle clé — copiez-la maintenant, elle ne sera plus jamais affichée :
221
+ </p>
222
+ <div className="mt-2 flex items-center gap-2">
223
+ <code className="min-w-0 flex-1 truncate rounded-lg border border-emerald-200 bg-white px-3 py-2 font-mono text-[13px] text-emerald-950">
224
+ {freshKey}
225
+ </code>
226
+ <Button size="sm" variant="outline" onClick={copyFreshKey}>
227
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
228
+ {copied ? "Copiée" : "Copier"}
229
+ </Button>
230
+ </div>
231
+ <button
232
+ type="button"
233
+ className="mt-2 text-xs text-emerald-700 underline underline-offset-2"
234
+ onClick={() => setFreshKey(null)}
235
+ >
236
+ J&apos;ai copié la clé, masquer
237
+ </button>
238
+ </div>
239
+ ) : null}
240
+
241
+ <div className="space-y-2">
242
+ <div className="flex items-end gap-2">
243
+ <div className="min-w-0 flex-1">
244
+ <p className="mb-1.5 text-[13px] font-medium text-slate-700">Nouvelle clé</p>
245
+ <input
246
+ className={inputCls}
247
+ placeholder="Nom (ex. Zapier production)"
248
+ value={name}
249
+ maxLength={80}
250
+ onChange={(e) => setName(e.target.value)}
251
+ onKeyDown={(e) => {
252
+ if (e.key === "Enter") createKey();
253
+ }}
254
+ />
255
+ </div>
256
+ <Button onClick={createKey} disabled={busy} className="shrink-0">
257
+ <Plus className="h-4 w-4" />
258
+ {busy ? "Création…" : "Créer la clé"}
259
+ </Button>
260
+ </div>
261
+ <div className="flex flex-wrap items-end gap-2">
262
+ <div className="min-w-0 flex-1">
263
+ <p className="mb-1.5 text-[13px] font-medium text-slate-700">Portée (scope)</p>
264
+ <select
265
+ className={inputCls}
266
+ value={scope}
267
+ onChange={(e) => setScope(e.target.value)}
268
+ >
269
+ <option value="full">Accès complet (full)</option>
270
+ <option value="crm:read">Lecture CRM (crm:read)</option>
271
+ <option value="crm:write">Lecture + écriture CRM (crm:write)</option>
272
+ <option value="tasks:run">Réveil des collaborateurs IA (tasks:run)</option>
273
+ </select>
274
+ </div>
275
+ {requiresUser ? (
276
+ <div className="min-w-0 flex-1">
277
+ <p className="mb-1.5 text-[13px] font-medium text-slate-700">
278
+ Agir au nom de
279
+ </p>
280
+ <select
281
+ className={inputCls}
282
+ value={linkedUser}
283
+ onChange={(e) => setLinkedUser(e.target.value)}
284
+ >
285
+ <option value="">— Choisir un utilisateur —</option>
286
+ {resolvedUsers.map((u) => (
287
+ <option key={u.id} value={u.id}>
288
+ {u.username}
289
+ {u.kind === "ai" ? " (IA)" : ""}
290
+ </option>
291
+ ))}
292
+ </select>
293
+ </div>
294
+ ) : null}
295
+ </div>
296
+ {requiresUser ? (
297
+ <p className="text-xs text-slate-500">
298
+ Cette clé pourra créer et suivre des tâches de collaborateurs IA au nom de
299
+ l&apos;utilisateur choisi (ex. depuis ChatGPT via le MCP, ou n8n).
300
+ </p>
301
+ ) : null}
302
+ </div>
303
+
304
+ {keys.length === 0 ? (
305
+ <p className="text-sm text-slate-500">
306
+ Aucune clé pour l&apos;instant. Créez-en une pour connecter Zapier, Make ou n8n.
307
+ </p>
308
+ ) : (
309
+ <ul className="divide-y divide-slate-100 rounded-xl border border-slate-200">
310
+ {keys.map((k) => (
311
+ <li key={k.id} className="flex items-center gap-3 px-4 py-3">
312
+ <div className="min-w-0 flex-1">
313
+ <p className="flex items-center gap-2 text-sm font-medium text-slate-900">
314
+ <span className="truncate">{k.name}</span>
315
+ {k.revoked_at ? (
316
+ <Badge variant="outline" className="border-red-200 bg-red-50 text-red-700">
317
+ Révoquée
318
+ </Badge>
319
+ ) : (
320
+ <Badge variant="outline" className="border-emerald-200 bg-emerald-50 text-emerald-700">
321
+ Active
322
+ </Badge>
323
+ )}
324
+ </p>
325
+ <p className="mt-0.5 truncate text-xs text-slate-500">
326
+ <code className="font-mono">{k.prefix}…</code>
327
+ {" · "}créée le {formatDate(k.created_at)}
328
+ {" · "}dernière utilisation : {formatDate(k.last_used_at)}
329
+ </p>
330
+ </div>
331
+ {!k.revoked_at ? (
332
+ <Button
333
+ size="sm"
334
+ variant="ghost"
335
+ className="shrink-0 text-red-600 hover:bg-red-50 hover:text-red-700"
336
+ onClick={() => revokeKey(k.id)}
337
+ >
338
+ <Trash2 className="h-4 w-4" />
339
+ Révoquer
340
+ </Button>
341
+ ) : null}
342
+ </li>
343
+ ))}
344
+ </ul>
345
+ )}
346
+ </CardContent>
347
+ </Card>
348
+ );
349
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Fonctionnement en arrière-plan — desktop uniquement (IPC background:*).
7
+ *
8
+ * - « Fermer = réduire dans la zone de notification » : la fenêtre se masque,
9
+ * le serveur local + le bridge restent actifs → les collaborateurs IA
10
+ * restent joignables (MCP / API) même fenêtre fermée.
11
+ * - « Lancer au démarrage » : app.setLoginItemSettings (Windows / macOS).
12
+ */
13
+
14
+ import { useEffect, useState } from "react";
15
+ import { Power } from "lucide-react";
16
+ import { toast } from "sonner";
17
+ import {
18
+ Card,
19
+ CardContent,
20
+ CardDescription,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from "../primitives/card";
24
+
25
+ type BackgroundState = {
26
+ closeToTray: boolean;
27
+ launchAtStartup: boolean;
28
+ trayActive: boolean;
29
+ platform: string;
30
+ };
31
+
32
+ export function DesktopBackgroundSettings() {
33
+ const [state, setState] = useState<BackgroundState | null>(null);
34
+ const [busy, setBusy] = useState(false);
35
+
36
+ useEffect(() => {
37
+ const api = getShellDesktopApi();
38
+ if (!api?.getBackgroundSettings) return;
39
+ void api
40
+ .getBackgroundSettings()
41
+ .then((s: BackgroundState) => setState(s))
42
+ .catch(() => {});
43
+ }, []);
44
+
45
+ // Navigateur classique ou vieux desktop sans l'IPC : section masquée.
46
+ if (!state) return null;
47
+
48
+ async function apply(patch: {
49
+ closeToTray?: boolean;
50
+ launchAtStartup?: boolean;
51
+ }) {
52
+ const api = getShellDesktopApi();
53
+ if (!api?.setBackgroundSettings) return;
54
+ setBusy(true);
55
+ try {
56
+ const r = await api.setBackgroundSettings(patch);
57
+ setState((prev) =>
58
+ prev
59
+ ? { ...prev, ...r.settings, trayActive: r.trayActive }
60
+ : prev,
61
+ );
62
+ toast.success("Réglage enregistré");
63
+ } catch (err) {
64
+ toast.error(err instanceof Error ? err.message : "Erreur");
65
+ } finally {
66
+ setBusy(false);
67
+ }
68
+ }
69
+
70
+ const launchSupported =
71
+ state.platform === "win32" || state.platform === "darwin";
72
+ const productName = getShellUiBrand().productName;
73
+
74
+ return (
75
+ <Card>
76
+ <CardHeader>
77
+ <CardTitle className="flex items-center gap-2 text-base">
78
+ <Power className="h-4 w-4" /> Fonctionnement en arrière-plan
79
+ </CardTitle>
80
+ <CardDescription>
81
+ Pour que vos collaborateurs IA restent joignables (tâches, MCP, API)
82
+ même quand la fenêtre est fermée, {productName} peut continuer de
83
+ tourner dans la zone de notification.
84
+ </CardDescription>
85
+ </CardHeader>
86
+ <CardContent className="space-y-4 text-sm">
87
+ <label className="flex items-start justify-between gap-4 leading-snug">
88
+ <div>
89
+ <span className="font-medium">
90
+ Fermer la fenêtre = réduire dans la zone de notification
91
+ </span>
92
+ <p className="text-xs text-muted-foreground">
93
+ {state.trayActive
94
+ ? `Le serveur local et les IA restent actifs ; quittez vraiment via le menu de l’icône ${productName}.`
95
+ : "Icône de notification indisponible sur ce système — fermer la fenêtre quittera l’application."}
96
+ </p>
97
+ </div>
98
+ <input
99
+ type="checkbox"
100
+ className="mt-1 h-4 w-4"
101
+ checked={state.closeToTray}
102
+ disabled={busy || !state.trayActive}
103
+ onChange={(e) => void apply({ closeToTray: e.target.checked })}
104
+ />
105
+ </label>
106
+ <label className="flex items-start justify-between gap-4 leading-snug">
107
+ <div>
108
+ <span className="font-medium">
109
+ Lancer {productName} au démarrage de l’ordinateur
110
+ </span>
111
+ <p className="text-xs text-muted-foreground">
112
+ {launchSupported
113
+ ? "L’application démarre avec votre session — vos IA sont opérationnelles sans intervention."
114
+ : `Non géré automatiquement sur ce système (ajoutez ${productName} aux applications de démarrage de votre environnement).`}
115
+ </p>
116
+ </div>
117
+ <input
118
+ type="checkbox"
119
+ className="mt-1 h-4 w-4"
120
+ checked={state.launchAtStartup}
121
+ disabled={busy || !launchSupported}
122
+ onChange={(e) => void apply({ launchAtStartup: e.target.checked })}
123
+ />
124
+ </label>
125
+ </CardContent>
126
+ </Card>
127
+ );
128
+ }