@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,801 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration en mode serveur web (navigateur → serveur headless Docker).
|
|
5
|
+
*
|
|
6
|
+
* Hors Electron, `getShellDesktopApi()` est absent : les panneaux Desktop*
|
|
7
|
+
* historiques se rendaient null et TOUS les onglets de Configuration étaient
|
|
8
|
+
* vides. Ces cartes consomment la surface HTTP que le serveur expose déjà
|
|
9
|
+
* (`/api/v1/os/*`, `/api/v1/core/version`, `/api/v1/platform/llm-keys`) pour
|
|
10
|
+
* rendre chaque onglet réel : statut Hermes/n8n, tunnel et URLs publiques,
|
|
11
|
+
* version d'image, clés IA (owner).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useEffect, useState } from "react";
|
|
15
|
+
import { toast } from "sonner";
|
|
16
|
+
import {
|
|
17
|
+
Bot,
|
|
18
|
+
Copy,
|
|
19
|
+
Globe,
|
|
20
|
+
KeyRound,
|
|
21
|
+
Loader2,
|
|
22
|
+
Network,
|
|
23
|
+
RefreshCw,
|
|
24
|
+
ServerCog,
|
|
25
|
+
Workflow,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import { getShellUiBrand } from "@creezio/shell-ui";
|
|
28
|
+
import { Button } from "../primitives/button";
|
|
29
|
+
import { Input } from "../primitives/input";
|
|
30
|
+
import {
|
|
31
|
+
Card,
|
|
32
|
+
CardContent,
|
|
33
|
+
CardDescription,
|
|
34
|
+
CardHeader,
|
|
35
|
+
CardTitle,
|
|
36
|
+
} from "../primitives/card";
|
|
37
|
+
|
|
38
|
+
async function getJson<T>(path: string): Promise<T | null> {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(path, { cache: "no-store" });
|
|
41
|
+
if (!res.ok) return null;
|
|
42
|
+
return (await res.json()) as T;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function StatusPill({ status, label }: { status: string; label: string }) {
|
|
49
|
+
const tone =
|
|
50
|
+
status === "running" || status === "ready"
|
|
51
|
+
? "border-emerald-200 bg-emerald-50 text-emerald-900"
|
|
52
|
+
: status === "installing" || status === "starting"
|
|
53
|
+
? "border-sky-200 bg-sky-50 text-sky-900"
|
|
54
|
+
: status === "missing" || status === "error"
|
|
55
|
+
? "border-amber-200 bg-amber-50 text-amber-900"
|
|
56
|
+
: "border-slate-200 bg-slate-50 text-slate-800";
|
|
57
|
+
return (
|
|
58
|
+
<span
|
|
59
|
+
className={`inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium ${tone}`}
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SERVICE_STATUS_LABELS: Record<string, string> = {
|
|
67
|
+
running: "En cours",
|
|
68
|
+
stopped: "Arrêté",
|
|
69
|
+
missing: "Runtime absent",
|
|
70
|
+
installing: "Installation…",
|
|
71
|
+
starting: "Démarrage…",
|
|
72
|
+
error: "Erreur",
|
|
73
|
+
ready: "Prêt",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/* ── Connexion : instance serveur ────────────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
type CoreVersion = {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
version: string;
|
|
81
|
+
architectureVersion?: string;
|
|
82
|
+
brandId?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Onglet Connexion en web : cette page EST le serveur — identité + version. */
|
|
86
|
+
export function ServerInstanceCard() {
|
|
87
|
+
const [version, setVersion] = useState<CoreVersion | null>(null);
|
|
88
|
+
const [origin, setOrigin] = useState("");
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setOrigin(window.location.origin);
|
|
92
|
+
void getJson<CoreVersion>("/api/v1/core/version").then(setVersion);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const productName = getShellUiBrand().productName;
|
|
96
|
+
return (
|
|
97
|
+
<Card>
|
|
98
|
+
<CardHeader className="pb-2">
|
|
99
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
100
|
+
<Network className="h-4 w-4" />
|
|
101
|
+
Serveur {productName}
|
|
102
|
+
</CardTitle>
|
|
103
|
+
<CardDescription>
|
|
104
|
+
Vous êtes connecté directement au serveur : tous les postes et
|
|
105
|
+
mobiles utilisent cette même URL. Le profil Héberger / Rejoindre ne
|
|
106
|
+
concerne que l'application desktop.
|
|
107
|
+
</CardDescription>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent className="space-y-2 text-sm">
|
|
110
|
+
<p className="rounded-md border bg-muted/40 px-3 py-2">
|
|
111
|
+
URL du serveur :{" "}
|
|
112
|
+
<span className="font-medium text-foreground">{origin || "…"}</span>
|
|
113
|
+
</p>
|
|
114
|
+
<p className="text-xs text-muted-foreground">
|
|
115
|
+
Version {version?.version ?? "…"}
|
|
116
|
+
{version?.architectureVersion
|
|
117
|
+
? ` · architecture ${version.architectureVersion}`
|
|
118
|
+
: ""}
|
|
119
|
+
{version?.brandId ? ` · ${version.brandId}` : ""}
|
|
120
|
+
</p>
|
|
121
|
+
</CardContent>
|
|
122
|
+
</Card>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Hermes / n8n : statut + ensure ──────────────────────────────────────── */
|
|
127
|
+
|
|
128
|
+
type ServiceKind = "hermes" | "n8n";
|
|
129
|
+
|
|
130
|
+
type ServiceStatusPayload = {
|
|
131
|
+
status?: string;
|
|
132
|
+
mode?: string;
|
|
133
|
+
version?: string | null;
|
|
134
|
+
detail?: string;
|
|
135
|
+
apiUrl?: string | null;
|
|
136
|
+
webuiUrl?: string | null;
|
|
137
|
+
uiUrl?: string | null;
|
|
138
|
+
bootstrapPhase?: string;
|
|
139
|
+
bootstrapError?: string | null;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ServiceStatusResponse = {
|
|
143
|
+
ok: boolean;
|
|
144
|
+
status?: ServiceStatusPayload;
|
|
145
|
+
nativeReady?: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const SERVICE_META: Record<
|
|
149
|
+
ServiceKind,
|
|
150
|
+
{ title: string; description: string; icon: typeof Bot }
|
|
151
|
+
> = {
|
|
152
|
+
hermes: {
|
|
153
|
+
title: "Hermes (agent IA)",
|
|
154
|
+
description:
|
|
155
|
+
"Cerveau IA embarqué du serveur : exécute les tâches IA du kanban et pilote le web.",
|
|
156
|
+
icon: Bot,
|
|
157
|
+
},
|
|
158
|
+
n8n: {
|
|
159
|
+
title: "n8n (automatisations)",
|
|
160
|
+
description:
|
|
161
|
+
"Moteur de workflows embarqué : webhooks, intégrations et automatisations.",
|
|
162
|
+
icon: Workflow,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Statut Hermes/n8n en web : GET /api/v1/os/{kind}/status + ensure. */
|
|
167
|
+
export function ServerServiceStatusCard({ kind }: { kind: ServiceKind }) {
|
|
168
|
+
const [payload, setPayload] = useState<ServiceStatusResponse | null>(null);
|
|
169
|
+
const [busy, setBusy] = useState(false);
|
|
170
|
+
const meta = SERVICE_META[kind];
|
|
171
|
+
const Icon = meta.icon;
|
|
172
|
+
|
|
173
|
+
const refresh = useCallback(async () => {
|
|
174
|
+
setPayload(await getJson<ServiceStatusResponse>(`/api/v1/os/${kind}/status`));
|
|
175
|
+
}, [kind]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
void refresh();
|
|
179
|
+
}, [refresh]);
|
|
180
|
+
|
|
181
|
+
async function onEnsure() {
|
|
182
|
+
setBusy(true);
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch(`/api/v1/os/${kind}/ensure`, { method: "POST" });
|
|
185
|
+
const body = (await res.json()) as { ok?: boolean; detail?: string };
|
|
186
|
+
if (body.ok) toast.success(body.detail || "Service vérifié");
|
|
187
|
+
else toast.error(body.detail || "Vérification en échec");
|
|
188
|
+
} catch (e) {
|
|
189
|
+
toast.error(e instanceof Error ? e.message : "Erreur réseau");
|
|
190
|
+
} finally {
|
|
191
|
+
setBusy(false);
|
|
192
|
+
await refresh();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const st = payload?.status;
|
|
197
|
+
const status = st?.status || (payload === null ? "" : "unknown");
|
|
198
|
+
return (
|
|
199
|
+
<Card>
|
|
200
|
+
<CardHeader className="pb-2">
|
|
201
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
202
|
+
<Icon className="h-4 w-4" />
|
|
203
|
+
{meta.title}
|
|
204
|
+
</CardTitle>
|
|
205
|
+
<CardDescription>{meta.description}</CardDescription>
|
|
206
|
+
</CardHeader>
|
|
207
|
+
<CardContent className="space-y-3 text-sm">
|
|
208
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
209
|
+
{status ? (
|
|
210
|
+
<StatusPill
|
|
211
|
+
status={status}
|
|
212
|
+
label={SERVICE_STATUS_LABELS[status] || status}
|
|
213
|
+
/>
|
|
214
|
+
) : (
|
|
215
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
216
|
+
)}
|
|
217
|
+
{st?.version ? (
|
|
218
|
+
<span className="text-xs text-muted-foreground">
|
|
219
|
+
v{st.version}
|
|
220
|
+
</span>
|
|
221
|
+
) : null}
|
|
222
|
+
{st?.mode ? (
|
|
223
|
+
<span className="text-xs text-muted-foreground">({st.mode})</span>
|
|
224
|
+
) : null}
|
|
225
|
+
</div>
|
|
226
|
+
{st?.detail ? (
|
|
227
|
+
<p className="text-xs text-muted-foreground">{st.detail}</p>
|
|
228
|
+
) : null}
|
|
229
|
+
{st?.bootstrapError ? (
|
|
230
|
+
<p className="text-xs text-destructive">{st.bootstrapError}</p>
|
|
231
|
+
) : null}
|
|
232
|
+
<div className="flex flex-wrap gap-2">
|
|
233
|
+
<Button
|
|
234
|
+
type="button"
|
|
235
|
+
size="sm"
|
|
236
|
+
variant="outline"
|
|
237
|
+
disabled={busy}
|
|
238
|
+
onClick={() => void onEnsure()}
|
|
239
|
+
>
|
|
240
|
+
{busy ? (
|
|
241
|
+
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
|
242
|
+
) : (
|
|
243
|
+
<RefreshCw className="mr-2 h-3.5 w-3.5" />
|
|
244
|
+
)}
|
|
245
|
+
Vérifier / démarrer
|
|
246
|
+
</Button>
|
|
247
|
+
</div>
|
|
248
|
+
</CardContent>
|
|
249
|
+
</Card>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── Tunnel : URLs publiques ─────────────────────────────────────────────── */
|
|
254
|
+
|
|
255
|
+
type TunnelStatusResponse = {
|
|
256
|
+
ok: boolean;
|
|
257
|
+
status?: {
|
|
258
|
+
configured?: boolean;
|
|
259
|
+
online?: boolean;
|
|
260
|
+
slug?: string | null;
|
|
261
|
+
hostname?: string | null;
|
|
262
|
+
publicUrl?: string | null;
|
|
263
|
+
publicUrls?: { crm?: string; n8n?: string; hermes?: string } | null;
|
|
264
|
+
error?: string | null;
|
|
265
|
+
} | null;
|
|
266
|
+
publicMcp?: string | null;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** true si la session courante est le compte propriétaire (non impersoné). */
|
|
270
|
+
async function fetchIsOwner(): Promise<boolean> {
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch("/api/v1/auth/me", { cache: "no-store" });
|
|
273
|
+
if (!res.ok) return false;
|
|
274
|
+
const me = (await res.json()) as {
|
|
275
|
+
role?: string;
|
|
276
|
+
impersonating?: boolean;
|
|
277
|
+
};
|
|
278
|
+
return me.role === "owner" && !me.impersonating;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const TUNNEL_SLUG_RE = /^[a-z0-9]([a-z0-9-]{0,46}[a-z0-9])$/;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Accès distant en web : GET /api/v1/os/tunnel/status + actions owner
|
|
288
|
+
* (/api/v1/platform/tunnel/* — miroir HTTP des IPC tunnel:* desktop).
|
|
289
|
+
*/
|
|
290
|
+
export function ServerTunnelCard() {
|
|
291
|
+
const [payload, setPayload] = useState<TunnelStatusResponse | null>(null);
|
|
292
|
+
const [owner, setOwner] = useState(false);
|
|
293
|
+
const [busy, setBusy] = useState(false);
|
|
294
|
+
const [slug, setSlug] = useState("");
|
|
295
|
+
const [checking, setChecking] = useState(false);
|
|
296
|
+
const [avail, setAvail] = useState<{
|
|
297
|
+
available: boolean;
|
|
298
|
+
reason?: string;
|
|
299
|
+
} | null>(null);
|
|
300
|
+
|
|
301
|
+
const refresh = useCallback(async () => {
|
|
302
|
+
setPayload(await getJson<TunnelStatusResponse>("/api/v1/os/tunnel/status"));
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
void refresh();
|
|
307
|
+
void fetchIsOwner().then(setOwner);
|
|
308
|
+
}, [refresh]);
|
|
309
|
+
|
|
310
|
+
async function copy(url: string) {
|
|
311
|
+
try {
|
|
312
|
+
await navigator.clipboard.writeText(url);
|
|
313
|
+
toast.success("URL copiée");
|
|
314
|
+
} catch {
|
|
315
|
+
toast.error("Impossible de copier l'URL");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function tunnelAction(action: "start" | "stop") {
|
|
320
|
+
setBusy(true);
|
|
321
|
+
try {
|
|
322
|
+
const res = await fetch(`/api/v1/platform/tunnel/${action}`, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
});
|
|
325
|
+
const body = (await res.json()) as { error?: string };
|
|
326
|
+
if (!res.ok) {
|
|
327
|
+
toast.error(body.error || "Action impossible");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
toast.success(
|
|
331
|
+
action === "start" ? "Tunnel démarré" : "Tunnel arrêté",
|
|
332
|
+
);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
toast.error(e instanceof Error ? e.message : "Erreur réseau");
|
|
335
|
+
} finally {
|
|
336
|
+
setBusy(false);
|
|
337
|
+
await refresh();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function checkSlug() {
|
|
342
|
+
const s = slug.trim().toLowerCase();
|
|
343
|
+
if (!TUNNEL_SLUG_RE.test(s)) {
|
|
344
|
+
setAvail({
|
|
345
|
+
available: false,
|
|
346
|
+
reason: "Slug invalide (a-z, 0-9, tirets, 2–48 car.)",
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
setChecking(true);
|
|
351
|
+
setAvail(null);
|
|
352
|
+
try {
|
|
353
|
+
const res = await fetch("/api/v1/platform/tunnel/check-slug", {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: { "Content-Type": "application/json" },
|
|
356
|
+
body: JSON.stringify({ slug: s }),
|
|
357
|
+
});
|
|
358
|
+
setAvail((await res.json()) as { available: boolean; reason?: string });
|
|
359
|
+
} catch (e) {
|
|
360
|
+
setAvail({
|
|
361
|
+
available: false,
|
|
362
|
+
reason: e instanceof Error ? e.message : "Erreur réseau",
|
|
363
|
+
});
|
|
364
|
+
} finally {
|
|
365
|
+
setChecking(false);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function reserve() {
|
|
370
|
+
const s = slug.trim().toLowerCase();
|
|
371
|
+
if (!TUNNEL_SLUG_RE.test(s)) {
|
|
372
|
+
toast.error("Slug invalide");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
setBusy(true);
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetch("/api/v1/platform/tunnel/reserve", {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" },
|
|
380
|
+
body: JSON.stringify({ slug: s }),
|
|
381
|
+
});
|
|
382
|
+
const body = (await res.json()) as {
|
|
383
|
+
ok?: boolean;
|
|
384
|
+
hostname?: string;
|
|
385
|
+
error?: string;
|
|
386
|
+
};
|
|
387
|
+
if (!res.ok || !body.ok) {
|
|
388
|
+
toast.error(body.error || "Réservation impossible");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
toast.success(`Réservé : ${body.hostname}`);
|
|
392
|
+
setSlug("");
|
|
393
|
+
setAvail(null);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
toast.error(e instanceof Error ? e.message : "Erreur réseau");
|
|
396
|
+
} finally {
|
|
397
|
+
setBusy(false);
|
|
398
|
+
await refresh();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const st = payload?.status;
|
|
403
|
+
const configured = st?.configured ?? false;
|
|
404
|
+
const online = st?.online ?? false;
|
|
405
|
+
return (
|
|
406
|
+
<Card>
|
|
407
|
+
<CardHeader className="pb-2">
|
|
408
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
409
|
+
<Globe className="h-4 w-4" />
|
|
410
|
+
Accès distant (tunnel)
|
|
411
|
+
</CardTitle>
|
|
412
|
+
<CardDescription>
|
|
413
|
+
URLs publiques HTTPS de ce serveur. Le propriétaire peut démarrer /
|
|
414
|
+
arrêter le tunnel ou réserver un nouveau sous-domaine.
|
|
415
|
+
</CardDescription>
|
|
416
|
+
</CardHeader>
|
|
417
|
+
<CardContent className="space-y-3 text-sm">
|
|
418
|
+
{payload === null ? (
|
|
419
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
420
|
+
) : !configured ? (
|
|
421
|
+
<p className="rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-slate-700">
|
|
422
|
+
Aucun tunnel configuré sur cette instance.
|
|
423
|
+
</p>
|
|
424
|
+
) : (
|
|
425
|
+
<>
|
|
426
|
+
<p
|
|
427
|
+
className={
|
|
428
|
+
online
|
|
429
|
+
? "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-emerald-800"
|
|
430
|
+
: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-amber-900"
|
|
431
|
+
}
|
|
432
|
+
>
|
|
433
|
+
{online
|
|
434
|
+
? `En ligne — ${st?.hostname || st?.publicUrl}`
|
|
435
|
+
: `Hors ligne — ${st?.hostname || "tunnel configuré"}`}
|
|
436
|
+
</p>
|
|
437
|
+
<ul className="space-y-1 font-mono text-[12px]">
|
|
438
|
+
{(
|
|
439
|
+
[
|
|
440
|
+
["CRM", st?.publicUrls?.crm || st?.publicUrl],
|
|
441
|
+
["n8n", st?.publicUrls?.n8n],
|
|
442
|
+
["Hermes", st?.publicUrls?.hermes],
|
|
443
|
+
["MCP", payload.publicMcp],
|
|
444
|
+
] as const
|
|
445
|
+
)
|
|
446
|
+
.filter(([, url]) => Boolean(url))
|
|
447
|
+
.map(([label, url]) => (
|
|
448
|
+
<li key={label} className="flex items-center gap-2">
|
|
449
|
+
<span className="w-14 shrink-0 font-sans text-xs text-muted-foreground">
|
|
450
|
+
{label}
|
|
451
|
+
</span>
|
|
452
|
+
<span className="truncate">{url}</span>
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
456
|
+
onClick={() => void copy(String(url))}
|
|
457
|
+
aria-label={`Copier l'URL ${label}`}
|
|
458
|
+
>
|
|
459
|
+
<Copy className="h-3.5 w-3.5" />
|
|
460
|
+
</button>
|
|
461
|
+
</li>
|
|
462
|
+
))}
|
|
463
|
+
</ul>
|
|
464
|
+
{owner ? (
|
|
465
|
+
<div className="flex flex-wrap gap-2">
|
|
466
|
+
{online ? (
|
|
467
|
+
<Button
|
|
468
|
+
size="sm"
|
|
469
|
+
variant="outline"
|
|
470
|
+
disabled={busy}
|
|
471
|
+
onClick={() => void tunnelAction("stop")}
|
|
472
|
+
>
|
|
473
|
+
{busy ? (
|
|
474
|
+
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
475
|
+
) : null}
|
|
476
|
+
Arrêter le tunnel
|
|
477
|
+
</Button>
|
|
478
|
+
) : (
|
|
479
|
+
<Button
|
|
480
|
+
size="sm"
|
|
481
|
+
disabled={busy}
|
|
482
|
+
onClick={() => void tunnelAction("start")}
|
|
483
|
+
>
|
|
484
|
+
{busy ? (
|
|
485
|
+
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
486
|
+
) : null}
|
|
487
|
+
Démarrer le tunnel
|
|
488
|
+
</Button>
|
|
489
|
+
)}
|
|
490
|
+
</div>
|
|
491
|
+
) : null}
|
|
492
|
+
</>
|
|
493
|
+
)}
|
|
494
|
+
{st?.error ? (
|
|
495
|
+
<p className="text-xs text-destructive">{st.error}</p>
|
|
496
|
+
) : null}
|
|
497
|
+
{owner && payload !== null ? (
|
|
498
|
+
<div className="space-y-2 rounded-md border border-dashed border-slate-300 p-3">
|
|
499
|
+
<p className="text-xs font-medium text-slate-700">
|
|
500
|
+
{configured
|
|
501
|
+
? "Réserver un autre sous-domaine (remplace l'actuel)"
|
|
502
|
+
: "Réserver un sous-domaine"}
|
|
503
|
+
</p>
|
|
504
|
+
<div className="flex items-center gap-2">
|
|
505
|
+
<Input
|
|
506
|
+
value={slug}
|
|
507
|
+
onChange={(e) => {
|
|
508
|
+
setSlug(
|
|
509
|
+
e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""),
|
|
510
|
+
);
|
|
511
|
+
setAvail(null);
|
|
512
|
+
}}
|
|
513
|
+
placeholder="mon-restaurant"
|
|
514
|
+
className="flex-1"
|
|
515
|
+
maxLength={48}
|
|
516
|
+
/>
|
|
517
|
+
</div>
|
|
518
|
+
<div className="flex flex-wrap gap-2">
|
|
519
|
+
<Button
|
|
520
|
+
size="sm"
|
|
521
|
+
variant="outline"
|
|
522
|
+
disabled={checking || !slug}
|
|
523
|
+
onClick={() => void checkSlug()}
|
|
524
|
+
>
|
|
525
|
+
{checking ? (
|
|
526
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
527
|
+
) : (
|
|
528
|
+
"Vérifier dispo"
|
|
529
|
+
)}
|
|
530
|
+
</Button>
|
|
531
|
+
<Button
|
|
532
|
+
size="sm"
|
|
533
|
+
disabled={busy || !avail?.available}
|
|
534
|
+
onClick={() => void reserve()}
|
|
535
|
+
>
|
|
536
|
+
{busy ? (
|
|
537
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
538
|
+
) : (
|
|
539
|
+
"Réserver & activer"
|
|
540
|
+
)}
|
|
541
|
+
</Button>
|
|
542
|
+
</div>
|
|
543
|
+
{avail ? (
|
|
544
|
+
<p
|
|
545
|
+
className={
|
|
546
|
+
avail.available
|
|
547
|
+
? "text-sm text-emerald-700"
|
|
548
|
+
: "text-sm text-amber-800"
|
|
549
|
+
}
|
|
550
|
+
>
|
|
551
|
+
{avail.available
|
|
552
|
+
? `Disponible : ${slug}`
|
|
553
|
+
: avail.reason || "Indisponible"}
|
|
554
|
+
</p>
|
|
555
|
+
) : null}
|
|
556
|
+
</div>
|
|
557
|
+
) : null}
|
|
558
|
+
</CardContent>
|
|
559
|
+
</Card>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ── Clés IA (owner) via /api/v1/platform/llm-keys ───────────────────────── */
|
|
564
|
+
|
|
565
|
+
type LlmKeysResponse = {
|
|
566
|
+
ok: boolean;
|
|
567
|
+
openai: { stored: boolean; active: boolean };
|
|
568
|
+
anthropic: { stored: boolean; active: boolean };
|
|
569
|
+
assistantReady: boolean;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
/** Clés IA en web : GET/PUT /api/v1/platform/llm-keys (owner uniquement). */
|
|
573
|
+
export function ServerLlmKeysCard() {
|
|
574
|
+
const [state, setState] = useState<LlmKeysResponse | null>(null);
|
|
575
|
+
const [forbidden, setForbidden] = useState(false);
|
|
576
|
+
const [drafts, setDrafts] = useState<Record<string, string>>({
|
|
577
|
+
openai: "",
|
|
578
|
+
anthropic: "",
|
|
579
|
+
});
|
|
580
|
+
const [saving, setSaving] = useState<string | null>(null);
|
|
581
|
+
|
|
582
|
+
const refresh = useCallback(async () => {
|
|
583
|
+
try {
|
|
584
|
+
const res = await fetch("/api/v1/platform/llm-keys", {
|
|
585
|
+
cache: "no-store",
|
|
586
|
+
});
|
|
587
|
+
if (res.status === 403 || res.status === 401) {
|
|
588
|
+
setForbidden(true);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (res.ok) setState((await res.json()) as LlmKeysResponse);
|
|
592
|
+
} catch {
|
|
593
|
+
/* réseau : la carte reste en chargement */
|
|
594
|
+
}
|
|
595
|
+
}, []);
|
|
596
|
+
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
void refresh();
|
|
599
|
+
}, [refresh]);
|
|
600
|
+
|
|
601
|
+
if (forbidden) {
|
|
602
|
+
return (
|
|
603
|
+
<Card>
|
|
604
|
+
<CardHeader className="pb-2">
|
|
605
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
606
|
+
<KeyRound className="h-4 w-4" /> Clés IA
|
|
607
|
+
</CardTitle>
|
|
608
|
+
</CardHeader>
|
|
609
|
+
<CardContent>
|
|
610
|
+
<p className="text-sm text-muted-foreground">
|
|
611
|
+
Réservé au compte propriétaire.
|
|
612
|
+
</p>
|
|
613
|
+
</CardContent>
|
|
614
|
+
</Card>
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function save(provider: "openai" | "anthropic") {
|
|
619
|
+
const key = drafts[provider].trim();
|
|
620
|
+
setSaving(provider);
|
|
621
|
+
try {
|
|
622
|
+
const res = await fetch("/api/v1/platform/llm-keys", {
|
|
623
|
+
method: "PUT",
|
|
624
|
+
headers: { "Content-Type": "application/json" },
|
|
625
|
+
body: JSON.stringify({ provider, key: key || null }),
|
|
626
|
+
});
|
|
627
|
+
const body = (await res.json()) as LlmKeysResponse & { error?: string };
|
|
628
|
+
if (!res.ok) {
|
|
629
|
+
toast.error(body.error || "Échec de l'enregistrement");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
setState(body);
|
|
633
|
+
setDrafts((d) => ({ ...d, [provider]: "" }));
|
|
634
|
+
toast.success(
|
|
635
|
+
key ? `Clé ${provider} enregistrée et active` : `Clé ${provider} supprimée`,
|
|
636
|
+
);
|
|
637
|
+
} catch (e) {
|
|
638
|
+
toast.error(e instanceof Error ? e.message : "Erreur réseau");
|
|
639
|
+
} finally {
|
|
640
|
+
setSaving(null);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const rows: Array<{
|
|
645
|
+
provider: "openai" | "anthropic";
|
|
646
|
+
name: string;
|
|
647
|
+
placeholder: string;
|
|
648
|
+
required: boolean;
|
|
649
|
+
}> = [
|
|
650
|
+
{ provider: "openai", name: "OpenAI", placeholder: "sk-…", required: true },
|
|
651
|
+
{
|
|
652
|
+
provider: "anthropic",
|
|
653
|
+
name: "Anthropic (optionnel)",
|
|
654
|
+
placeholder: "sk-ant-…",
|
|
655
|
+
required: false,
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<Card>
|
|
661
|
+
<CardHeader className="pb-2">
|
|
662
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
663
|
+
<KeyRound className="h-4 w-4" /> Clés IA (Bring Your Own Key)
|
|
664
|
+
</CardTitle>
|
|
665
|
+
<CardDescription>
|
|
666
|
+
Clés stockées chiffrées sur le serveur, jamais envoyées ailleurs que
|
|
667
|
+
chez le fournisseur du modèle. L'assistant les utilise
|
|
668
|
+
immédiatement ; Hermes et les embeds au prochain redémarrage.
|
|
669
|
+
</CardDescription>
|
|
670
|
+
</CardHeader>
|
|
671
|
+
<CardContent className="space-y-3">
|
|
672
|
+
{state === null ? (
|
|
673
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
674
|
+
) : (
|
|
675
|
+
<p
|
|
676
|
+
className={
|
|
677
|
+
state.assistantReady
|
|
678
|
+
? "rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800"
|
|
679
|
+
: "rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900"
|
|
680
|
+
}
|
|
681
|
+
>
|
|
682
|
+
{state.assistantReady
|
|
683
|
+
? "Assistant : prêt (clé OpenAI active côté serveur)"
|
|
684
|
+
: "Assistant : désactivé — clé OpenAI requise"}
|
|
685
|
+
</p>
|
|
686
|
+
)}
|
|
687
|
+
{rows.map(({ provider, name, placeholder, required }) => {
|
|
688
|
+
const info = state?.[provider];
|
|
689
|
+
const busy = saving === provider;
|
|
690
|
+
return (
|
|
691
|
+
<div key={provider} className="flex items-center gap-2">
|
|
692
|
+
<span className="w-44 shrink-0 text-sm">
|
|
693
|
+
{name}
|
|
694
|
+
{required ? (
|
|
695
|
+
<span className="ml-1 text-xs text-amber-700">requis</span>
|
|
696
|
+
) : null}
|
|
697
|
+
<span
|
|
698
|
+
className={
|
|
699
|
+
info?.active
|
|
700
|
+
? "ml-1 text-xs text-emerald-600"
|
|
701
|
+
: "ml-1 text-xs text-muted-foreground"
|
|
702
|
+
}
|
|
703
|
+
>
|
|
704
|
+
(
|
|
705
|
+
{info?.active
|
|
706
|
+
? "active"
|
|
707
|
+
: info?.stored
|
|
708
|
+
? "enregistrée"
|
|
709
|
+
: "non configuré"}
|
|
710
|
+
)
|
|
711
|
+
</span>
|
|
712
|
+
</span>
|
|
713
|
+
<Input
|
|
714
|
+
type="password"
|
|
715
|
+
value={drafts[provider]}
|
|
716
|
+
onChange={(e) =>
|
|
717
|
+
setDrafts((d) => ({ ...d, [provider]: e.target.value }))
|
|
718
|
+
}
|
|
719
|
+
placeholder={info?.stored ? "•••••• (remplacer)" : placeholder}
|
|
720
|
+
className="flex-1"
|
|
721
|
+
disabled={busy}
|
|
722
|
+
/>
|
|
723
|
+
<Button
|
|
724
|
+
size="sm"
|
|
725
|
+
variant="outline"
|
|
726
|
+
disabled={busy || state === null}
|
|
727
|
+
onClick={() => void save(provider)}
|
|
728
|
+
>
|
|
729
|
+
{busy ? (
|
|
730
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
731
|
+
) : drafts[provider].trim() ? (
|
|
732
|
+
"Enregistrer"
|
|
733
|
+
) : info?.stored ? (
|
|
734
|
+
"Supprimer"
|
|
735
|
+
) : (
|
|
736
|
+
"Enregistrer"
|
|
737
|
+
)}
|
|
738
|
+
</Button>
|
|
739
|
+
</div>
|
|
740
|
+
);
|
|
741
|
+
})}
|
|
742
|
+
</CardContent>
|
|
743
|
+
</Card>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* ── Système : version + opérations gérées par l'opérateur ───────────────── */
|
|
748
|
+
|
|
749
|
+
/** Version d'image en web — remplace l'auto-update Electron. */
|
|
750
|
+
export function ServerVersionCard() {
|
|
751
|
+
const [version, setVersion] = useState<CoreVersion | null>(null);
|
|
752
|
+
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
void getJson<CoreVersion>("/api/v1/core/version").then(setVersion);
|
|
755
|
+
}, []);
|
|
756
|
+
|
|
757
|
+
return (
|
|
758
|
+
<Card>
|
|
759
|
+
<CardHeader className="pb-2">
|
|
760
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
761
|
+
<ServerCog className="h-4 w-4" /> Version du serveur
|
|
762
|
+
</CardTitle>
|
|
763
|
+
<CardDescription>
|
|
764
|
+
Les mises à jour de ce serveur sont déployées par l'opérateur
|
|
765
|
+
(admin flotte) — rien à faire de votre côté.
|
|
766
|
+
</CardDescription>
|
|
767
|
+
</CardHeader>
|
|
768
|
+
<CardContent>
|
|
769
|
+
<p className="text-sm">
|
|
770
|
+
Version installée :{" "}
|
|
771
|
+
<span className="font-medium">{version?.version ?? "…"}</span>
|
|
772
|
+
{version?.architectureVersion ? (
|
|
773
|
+
<span className="text-xs text-muted-foreground">
|
|
774
|
+
{" "}
|
|
775
|
+
· architecture {version.architectureVersion}
|
|
776
|
+
</span>
|
|
777
|
+
) : null}
|
|
778
|
+
</p>
|
|
779
|
+
</CardContent>
|
|
780
|
+
</Card>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Encart « opération gérée par l'opérateur serveur » (web). */
|
|
785
|
+
export function ServerOperatorNotice({ label }: { label: string }) {
|
|
786
|
+
const productName = getShellUiBrand().productName;
|
|
787
|
+
return (
|
|
788
|
+
<div className="flex items-start gap-3 rounded-lg border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600">
|
|
789
|
+
<ServerCog className="mt-0.5 h-5 w-5 shrink-0 text-slate-400" />
|
|
790
|
+
<div>
|
|
791
|
+
<p className="font-medium text-slate-700">
|
|
792
|
+
Géré par l'opérateur {productName}
|
|
793
|
+
</p>
|
|
794
|
+
<p className="mt-1">
|
|
795
|
+
Sur un serveur hébergé, {label} relève de l'opérateur de
|
|
796
|
+
l'instance (admin flotte), pas de cette interface.
|
|
797
|
+
</p>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
}
|