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