@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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub screencast des espaces IA — côté serveur Next (mémoire process).
|
|
3
|
+
*
|
|
4
|
+
* L'app desktop POSTe des frames JPEG (base64) via
|
|
5
|
+
* POST /api/v1/desktop/screencast/frame ; les spectateurs s'abonnent en SSE
|
|
6
|
+
* via GET /api/v1/tasks/screencast/:aiUserId/stream.
|
|
7
|
+
*
|
|
8
|
+
* Backpressure : on ne garde QUE la dernière frame par IA. Chaque spectateur
|
|
9
|
+
* a un slot « latest » écrasé à chaque publication ; la livraison est
|
|
10
|
+
* planifiée (setImmediate) et n'envoie que la frame la plus récente — un
|
|
11
|
+
* spectateur lent saute des frames au lieu d'accumuler une file.
|
|
12
|
+
*
|
|
13
|
+
* Singleton globalThis : survit au HMR dev, simple module en prod.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ScreencastFrame = {
|
|
17
|
+
/** JPEG base64 (sans préfixe data:). */
|
|
18
|
+
data: string;
|
|
19
|
+
seq: number;
|
|
20
|
+
ts: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Viewer = {
|
|
24
|
+
id: number;
|
|
25
|
+
onFrame: (frame: ScreencastFrame) => void;
|
|
26
|
+
latest: ScreencastFrame | null;
|
|
27
|
+
scheduled: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Channel = {
|
|
31
|
+
viewers: Map<number, Viewer>;
|
|
32
|
+
lastFrame: ScreencastFrame | null;
|
|
33
|
+
seq: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const g = globalThis as unknown as {
|
|
37
|
+
__tf2ScreencastHub?: Map<string, Channel>;
|
|
38
|
+
__tf2ScreencastViewerId?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function hub(): Map<string, Channel> {
|
|
42
|
+
if (!g.__tf2ScreencastHub) g.__tf2ScreencastHub = new Map();
|
|
43
|
+
return g.__tf2ScreencastHub;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function channel(aiUserId: string): Channel {
|
|
47
|
+
const h = hub();
|
|
48
|
+
let ch = h.get(aiUserId);
|
|
49
|
+
if (!ch) {
|
|
50
|
+
ch = { viewers: new Map(), lastFrame: null, seq: 0 };
|
|
51
|
+
h.set(aiUserId, ch);
|
|
52
|
+
}
|
|
53
|
+
return ch;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function nextViewerId(): number {
|
|
57
|
+
g.__tf2ScreencastViewerId = (g.__tf2ScreencastViewerId || 0) + 1;
|
|
58
|
+
return g.__tf2ScreencastViewerId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Planifie la livraison de la dernière frame au spectateur (skip si déjà planifiée). */
|
|
62
|
+
function scheduleDeliver(viewer: Viewer): void {
|
|
63
|
+
if (viewer.scheduled) return;
|
|
64
|
+
viewer.scheduled = true;
|
|
65
|
+
setImmediate(() => {
|
|
66
|
+
viewer.scheduled = false;
|
|
67
|
+
const frame = viewer.latest;
|
|
68
|
+
viewer.latest = null;
|
|
69
|
+
if (!frame) return;
|
|
70
|
+
try {
|
|
71
|
+
viewer.onFrame(frame);
|
|
72
|
+
} catch {
|
|
73
|
+
/* spectateur mort — l'unsubscribe SSE fera le ménage */
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Publie une frame (appelé par la route desktop). Retourne le nombre de
|
|
80
|
+
* spectateurs — l'app desktop s'auto-arrête si 0 plusieurs fois de suite.
|
|
81
|
+
*/
|
|
82
|
+
export function publishScreencastFrame(
|
|
83
|
+
aiUserId: string,
|
|
84
|
+
dataB64: string,
|
|
85
|
+
): { viewers: number; seq: number } {
|
|
86
|
+
const ch = channel(aiUserId);
|
|
87
|
+
ch.seq += 1;
|
|
88
|
+
const frame: ScreencastFrame = { data: dataB64, seq: ch.seq, ts: Date.now() };
|
|
89
|
+
ch.lastFrame = frame;
|
|
90
|
+
ch.viewers.forEach((viewer) => {
|
|
91
|
+
viewer.latest = frame;
|
|
92
|
+
scheduleDeliver(viewer);
|
|
93
|
+
});
|
|
94
|
+
return { viewers: ch.viewers.size, seq: ch.seq };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Abonne un spectateur. La dernière frame connue est livrée immédiatement
|
|
99
|
+
* (affichage instantané si le screencast tourne déjà). Retourne l'unsubscribe.
|
|
100
|
+
*/
|
|
101
|
+
export function subscribeScreencast(
|
|
102
|
+
aiUserId: string,
|
|
103
|
+
onFrame: (frame: ScreencastFrame) => void,
|
|
104
|
+
): () => void {
|
|
105
|
+
const ch = channel(aiUserId);
|
|
106
|
+
const viewer: Viewer = {
|
|
107
|
+
id: nextViewerId(),
|
|
108
|
+
onFrame,
|
|
109
|
+
latest: ch.lastFrame,
|
|
110
|
+
scheduled: false,
|
|
111
|
+
};
|
|
112
|
+
ch.viewers.set(viewer.id, viewer);
|
|
113
|
+
if (viewer.latest) scheduleDeliver(viewer);
|
|
114
|
+
return () => {
|
|
115
|
+
ch.viewers.delete(viewer.id);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function screencastViewerCount(aiUserId: string): number {
|
|
120
|
+
return hub().get(aiUserId)?.viewers.size || 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Purge la dernière frame (fin de session — évite une image périmée). */
|
|
124
|
+
export function clearScreencastFrame(aiUserId: string): void {
|
|
125
|
+
const ch = hub().get(aiUserId);
|
|
126
|
+
if (ch) ch.lastFrame = null;
|
|
127
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
4
|
+
/**
|
|
5
|
+
* Ouverture du workspace visuel d'un collaborateur IA (« Voir comme IA »).
|
|
6
|
+
* Client uniquement — nécessite le bridge desktop (app Electron).
|
|
7
|
+
*/
|
|
8
|
+
export function aiWorkspaceAvailable(): boolean {
|
|
9
|
+
return (
|
|
10
|
+
typeof window !== "undefined" &&
|
|
11
|
+
Boolean(getShellDesktopApi()?.showAiWorkspace)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function openAiWorkspaceView(
|
|
16
|
+
userId: string,
|
|
17
|
+
label?: string,
|
|
18
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
19
|
+
const desktop =
|
|
20
|
+
typeof window !== "undefined" ? getShellDesktopApi() : undefined;
|
|
21
|
+
if (!desktop?.showAiWorkspace) {
|
|
22
|
+
return { ok: false, error: "Disponible uniquement dans l'app desktop" };
|
|
23
|
+
}
|
|
24
|
+
const session = await fetch("/api/v1/auth/ai-workspace-session", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ userId }),
|
|
28
|
+
});
|
|
29
|
+
const data = (await session.json().catch(() => ({}))) as {
|
|
30
|
+
token?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
if (!session.ok || !data.token) {
|
|
34
|
+
return { ok: false, error: data.error || "Session workspace refusée" };
|
|
35
|
+
}
|
|
36
|
+
const ensured = await desktop.ensureAiWorkspace?.({
|
|
37
|
+
userId,
|
|
38
|
+
token: data.token,
|
|
39
|
+
label,
|
|
40
|
+
});
|
|
41
|
+
if (!ensured?.ok) {
|
|
42
|
+
return { ok: false, error: "Workspace IA indisponible" };
|
|
43
|
+
}
|
|
44
|
+
await desktop.showAiWorkspace(userId);
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
package/ui/lib/aid.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribut data-* analytics / UiDriver — SoT = configureShellUiBrand({ aidAttr }).
|
|
3
|
+
* Défaut kit : data-creezio-aid (plus de data-tf2-aid hardcodé).
|
|
4
|
+
*/
|
|
5
|
+
import { getShellUiBrand } from "@creezio/shell-ui";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_AID_ATTR = "data-creezio-aid";
|
|
8
|
+
|
|
9
|
+
export function resolveAidAttr(): string {
|
|
10
|
+
return getShellUiBrand().aidAttr ?? DEFAULT_AID_ATTR;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Props React pour un élément instrumenté : { [aidAttr]: value }. */
|
|
14
|
+
export function aidProps(value: string): Record<string, string> {
|
|
15
|
+
return { [resolveAidAttr()]: value };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Sélecteur CSS pour tout élément portant l'attribut aid courant. */
|
|
19
|
+
export function aidSelector(value?: string): string {
|
|
20
|
+
const attr = resolveAidAttr();
|
|
21
|
+
return value != null ? `[${attr}="${value}"]` : `[${attr}]`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
4
|
+
/**
|
|
5
|
+
* Détection partagée « cette page tourne-t-elle dans un desktop client
|
|
6
|
+
* distant ? » (app Client kind=client, ou legacy joint en remote).
|
|
7
|
+
*
|
|
8
|
+
* - Navigateur pur (pas de getShellDesktopApi()) → l'UI est servie par le
|
|
9
|
+
* serveur : on considère « hôte » (les panneaux desktop se rendent null
|
|
10
|
+
* d'eux-mêmes hors Electron).
|
|
11
|
+
* - Desktop avec profil remote → client distant : les sections/actions qui
|
|
12
|
+
* pilotent la stack locale (Hermes, n8n, tunnel, plugins, reindex…) sont
|
|
13
|
+
* gérées par l'app Serveur, pas par ce poste.
|
|
14
|
+
*
|
|
15
|
+
* Résultat mis en cache pour la durée de vie de la page : le mode de
|
|
16
|
+
* connexion ne change jamais sans relance de l'app.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type DesktopHostInfo = {
|
|
20
|
+
/** true si getShellDesktopApi() existe (Electron). */
|
|
21
|
+
isDesktop: boolean;
|
|
22
|
+
/** true si les actions host-only ont un sens ici (hôte local ou navigateur). */
|
|
23
|
+
isHost: boolean;
|
|
24
|
+
appKind: "server" | "client" | "legacy" | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let cached: Promise<DesktopHostInfo> | null = null;
|
|
28
|
+
|
|
29
|
+
export function getDesktopHostInfo(): Promise<DesktopHostInfo> {
|
|
30
|
+
if (cached) return cached;
|
|
31
|
+
cached = (async (): Promise<DesktopHostInfo> => {
|
|
32
|
+
if (typeof window === "undefined") {
|
|
33
|
+
return { isDesktop: false, isHost: true, appKind: null };
|
|
34
|
+
}
|
|
35
|
+
const api = getShellDesktopApi();
|
|
36
|
+
if (!api?.getConnectionProfile) {
|
|
37
|
+
return { isDesktop: false, isHost: true, appKind: null };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const [profile, info] = await Promise.all([
|
|
41
|
+
api.getConnectionProfile(),
|
|
42
|
+
api.getInfo?.() ?? Promise.resolve(undefined),
|
|
43
|
+
]);
|
|
44
|
+
return {
|
|
45
|
+
isDesktop: true,
|
|
46
|
+
isHost: profile.mode !== "remote",
|
|
47
|
+
appKind: info?.appKind ?? null,
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
// IPC indisponible : ne pas casser l'UI, comportement hôte par défaut.
|
|
51
|
+
return { isDesktop: true, isHost: true, appKind: null };
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Raccourci : true quand ce poste est un client distant (host-only à masquer). */
|
|
58
|
+
export async function isRemoteDesktopClient(): Promise<boolean> {
|
|
59
|
+
const info = await getDesktopHostInfo();
|
|
60
|
+
return info.isDesktop && !info.isHost;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ── Miroir télémétrie flotte ────────────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
export type FleetActionPayload = {
|
|
66
|
+
type: string;
|
|
67
|
+
label: string;
|
|
68
|
+
path?: string;
|
|
69
|
+
userId?: string;
|
|
70
|
+
username?: string;
|
|
71
|
+
meta?: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** null = résolution hôte en cours (on bufferise), sinon décision figée. */
|
|
75
|
+
let fleetMirrorAllowed: boolean | null = null;
|
|
76
|
+
let fleetMirrorBuffer: FleetActionPayload[] = [];
|
|
77
|
+
const FLEET_MIRROR_BUFFER_MAX = 20;
|
|
78
|
+
|
|
79
|
+
function sendFleetAction(payload: FleetActionPayload): void {
|
|
80
|
+
const api = typeof window !== "undefined" ? getShellDesktopApi() : undefined;
|
|
81
|
+
if (!api?.reportFleetAction) return;
|
|
82
|
+
// catch : best-effort — jamais de rejet non géré pour de la télémétrie.
|
|
83
|
+
void api.reportFleetAction(payload).catch(() => {});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Miroir flotte gaté : sur un client distant (app Client / legacy joint en
|
|
88
|
+
* remote), fleet:action est un canal host-only — on n'appelle simplement pas
|
|
89
|
+
* l'IPC au lieu de générer un rejet à chaque page vue / clic.
|
|
90
|
+
*/
|
|
91
|
+
export function mirrorFleetAction(payload: FleetActionPayload): void {
|
|
92
|
+
if (typeof window === "undefined") return;
|
|
93
|
+
if (!getShellDesktopApi()?.reportFleetAction) return;
|
|
94
|
+
if (fleetMirrorAllowed === true) {
|
|
95
|
+
sendFleetAction(payload);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (fleetMirrorAllowed === false) return;
|
|
99
|
+
// Résolution en cours : bufferiser les premiers événements pour ne pas
|
|
100
|
+
// perdre le page.view initial côté hôte.
|
|
101
|
+
fleetMirrorBuffer.push(payload);
|
|
102
|
+
if (fleetMirrorBuffer.length > FLEET_MIRROR_BUFFER_MAX) {
|
|
103
|
+
fleetMirrorBuffer = fleetMirrorBuffer.slice(-FLEET_MIRROR_BUFFER_MAX);
|
|
104
|
+
}
|
|
105
|
+
if (fleetMirrorBuffer.length > 1) return; // résolution déjà lancée
|
|
106
|
+
void isRemoteDesktopClient().then((remote) => {
|
|
107
|
+
fleetMirrorAllowed = !remote;
|
|
108
|
+
const pending = fleetMirrorBuffer;
|
|
109
|
+
fleetMirrorBuffer = [];
|
|
110
|
+
if (fleetMirrorAllowed) {
|
|
111
|
+
for (const p of pending) sendFleetAction(p);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
2
|
+
/**
|
|
3
|
+
* Tracker produit flotte (renderer) — page views, clics, session.
|
|
4
|
+
* Envoie via getShellDesktopApi().reportFleetAction (opt-in côté main).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mirrorFleetAction } from "./desktop-host";
|
|
8
|
+
import { resolveAidAttr } from "./aid";
|
|
9
|
+
|
|
10
|
+
type FleetReport = {
|
|
11
|
+
type?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
label: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
referrerPath?: string;
|
|
17
|
+
userId?: string;
|
|
18
|
+
username?: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
surface?: string;
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
meta?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type Session = {
|
|
26
|
+
userId: string | null;
|
|
27
|
+
username: string | null;
|
|
28
|
+
sessionId: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let session: Session = {
|
|
32
|
+
userId: null,
|
|
33
|
+
username: null,
|
|
34
|
+
sessionId: null,
|
|
35
|
+
};
|
|
36
|
+
let currentPath: string | null = null;
|
|
37
|
+
let pageEnteredAt = 0;
|
|
38
|
+
let clickBound = false;
|
|
39
|
+
let visibilityBound = false;
|
|
40
|
+
|
|
41
|
+
function report(payload: FleetReport): void {
|
|
42
|
+
// Gaté : sur un client distant, fleet:action est host-only (voir desktop-host).
|
|
43
|
+
mirrorFleetAction({
|
|
44
|
+
type: payload.type || payload.name || "event",
|
|
45
|
+
label: payload.label,
|
|
46
|
+
path: payload.path,
|
|
47
|
+
userId: payload.userId ?? session.userId ?? undefined,
|
|
48
|
+
username: payload.username ?? session.username ?? undefined,
|
|
49
|
+
meta: {
|
|
50
|
+
...(payload.meta || {}),
|
|
51
|
+
name: payload.name || payload.type,
|
|
52
|
+
category: payload.category || "ui",
|
|
53
|
+
sessionId: payload.sessionId ?? session.sessionId,
|
|
54
|
+
surface: payload.surface || "crm",
|
|
55
|
+
referrerPath: payload.referrerPath,
|
|
56
|
+
durationMs: payload.durationMs,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setFleetTrackerSession(next: {
|
|
62
|
+
userId?: string | null;
|
|
63
|
+
username?: string | null;
|
|
64
|
+
sessionId?: string | null;
|
|
65
|
+
}): void {
|
|
66
|
+
const prev = session.userId;
|
|
67
|
+
session = {
|
|
68
|
+
userId: next.userId !== undefined ? next.userId : session.userId,
|
|
69
|
+
username: next.username !== undefined ? next.username : session.username,
|
|
70
|
+
sessionId: next.sessionId !== undefined ? next.sessionId : session.sessionId,
|
|
71
|
+
};
|
|
72
|
+
if (session.userId && session.userId !== prev) {
|
|
73
|
+
report({
|
|
74
|
+
name: "session.start",
|
|
75
|
+
type: "session.start",
|
|
76
|
+
category: "auth",
|
|
77
|
+
label: `Session ${session.username || session.userId}`,
|
|
78
|
+
path: typeof location !== "undefined" ? location.pathname : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function closePage(reason: string): void {
|
|
84
|
+
if (!currentPath || !pageEnteredAt) return;
|
|
85
|
+
const durationMs = Date.now() - pageEnteredAt;
|
|
86
|
+
report({
|
|
87
|
+
name: "page.hide",
|
|
88
|
+
type: "page.hide",
|
|
89
|
+
category: "navigation",
|
|
90
|
+
label: `Quitte ${currentPath}`,
|
|
91
|
+
path: currentPath,
|
|
92
|
+
durationMs,
|
|
93
|
+
meta: { reason },
|
|
94
|
+
});
|
|
95
|
+
pageEnteredAt = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function trackFleetPageView(path: string): void {
|
|
99
|
+
const next = String(path || "").slice(0, 300);
|
|
100
|
+
if (!next) return;
|
|
101
|
+
if (currentPath === next && pageEnteredAt) return;
|
|
102
|
+
const prev = currentPath;
|
|
103
|
+
if (prev) closePage("navigate");
|
|
104
|
+
currentPath = next;
|
|
105
|
+
pageEnteredAt = Date.now();
|
|
106
|
+
report({
|
|
107
|
+
name: "page.view",
|
|
108
|
+
type: "page.view",
|
|
109
|
+
category: "navigation",
|
|
110
|
+
label: `Page ${next}`,
|
|
111
|
+
path: next,
|
|
112
|
+
referrerPath: prev || undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function trackFleetEvent(payload: FleetReport): void {
|
|
117
|
+
report(payload);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clickLabel(el: Element): string {
|
|
121
|
+
const aid = el.getAttribute(resolveAidAttr());
|
|
122
|
+
if (aid) return aid;
|
|
123
|
+
const aria = el.getAttribute("aria-label");
|
|
124
|
+
if (aria) return aria.slice(0, 120);
|
|
125
|
+
const title = el.getAttribute("title");
|
|
126
|
+
if (title) return title.slice(0, 120);
|
|
127
|
+
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
128
|
+
if (text) return text.slice(0, 120);
|
|
129
|
+
const tag = el.tagName.toLowerCase();
|
|
130
|
+
const href = el.getAttribute("href");
|
|
131
|
+
if (href) return `${tag} ${href}`.slice(0, 120);
|
|
132
|
+
return tag;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onClickCapture(ev: MouseEvent): void {
|
|
136
|
+
const t = ev.target;
|
|
137
|
+
if (!(t instanceof Element)) return;
|
|
138
|
+
const el =
|
|
139
|
+
t.closest(
|
|
140
|
+
`[${resolveAidAttr()}],button,a,[role='button'],[role='tab'],input[type='submit']`,
|
|
141
|
+
) || null;
|
|
142
|
+
if (!el) return;
|
|
143
|
+
// Ignore chrome fenêtre / inputs texte purs
|
|
144
|
+
if (el.closest("." + getShellUiBrand().titlebarNoDragClass)) return;
|
|
145
|
+
if (
|
|
146
|
+
el instanceof HTMLInputElement &&
|
|
147
|
+
el.type !== "submit" &&
|
|
148
|
+
el.type !== "button" &&
|
|
149
|
+
el.type !== "checkbox" &&
|
|
150
|
+
el.type !== "radio"
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const aid = el.getAttribute(resolveAidAttr()) || undefined;
|
|
155
|
+
report({
|
|
156
|
+
name: "ui.click",
|
|
157
|
+
type: "ui.click",
|
|
158
|
+
category: "ui",
|
|
159
|
+
label: clickLabel(el),
|
|
160
|
+
path: currentPath || location.pathname,
|
|
161
|
+
meta: {
|
|
162
|
+
aid: aid || null,
|
|
163
|
+
tag: el.tagName.toLowerCase(),
|
|
164
|
+
role: el.getAttribute("role") || null,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onVisibility(): void {
|
|
170
|
+
if (document.visibilityState === "hidden") {
|
|
171
|
+
closePage("hidden");
|
|
172
|
+
} else if (document.visibilityState === "visible" && currentPath) {
|
|
173
|
+
pageEnteredAt = Date.now();
|
|
174
|
+
report({
|
|
175
|
+
name: "page.view",
|
|
176
|
+
type: "page.view",
|
|
177
|
+
category: "navigation",
|
|
178
|
+
label: `Retour ${currentPath}`,
|
|
179
|
+
path: currentPath,
|
|
180
|
+
meta: { reason: "visible" },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function ensureFleetTrackerDom(): void {
|
|
186
|
+
if (typeof document === "undefined") return;
|
|
187
|
+
if (!clickBound) {
|
|
188
|
+
document.addEventListener("click", onClickCapture, true);
|
|
189
|
+
clickBound = true;
|
|
190
|
+
}
|
|
191
|
+
if (!visibilityBound) {
|
|
192
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
193
|
+
visibilityBound = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function trackFleetCommerce(opts: {
|
|
198
|
+
action: string;
|
|
199
|
+
entityType: string;
|
|
200
|
+
entityId?: string | number;
|
|
201
|
+
label?: string;
|
|
202
|
+
meta?: Record<string, unknown>;
|
|
203
|
+
}): void {
|
|
204
|
+
report({
|
|
205
|
+
name: `commerce.${opts.action}`,
|
|
206
|
+
type: `commerce.${opts.action}`,
|
|
207
|
+
category: "commerce",
|
|
208
|
+
label:
|
|
209
|
+
opts.label ||
|
|
210
|
+
`${opts.action} ${opts.entityType}${opts.entityId != null ? ` #${opts.entityId}` : ""}`,
|
|
211
|
+
path: currentPath || undefined,
|
|
212
|
+
meta: {
|
|
213
|
+
entityType: opts.entityType,
|
|
214
|
+
entityId: opts.entityId != null ? String(opts.entityId) : null,
|
|
215
|
+
...(opts.meta || {}),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
// Persistance locale (Admin → Analytics) en plus du miroir flotte.
|
|
219
|
+
void import("@creezio/observability/ui")
|
|
220
|
+
.then(({ trackUsageEvent }) => {
|
|
221
|
+
trackUsageEvent({
|
|
222
|
+
eventType: `commerce.${opts.action}`,
|
|
223
|
+
category: "commerce",
|
|
224
|
+
label:
|
|
225
|
+
opts.label ||
|
|
226
|
+
`${opts.action} ${opts.entityType}${opts.entityId != null ? ` #${opts.entityId}` : ""}`,
|
|
227
|
+
meta: {
|
|
228
|
+
entityType: opts.entityType,
|
|
229
|
+
entityId: opts.entityId != null ? String(opts.entityId) : null,
|
|
230
|
+
...(opts.meta || {}),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
})
|
|
234
|
+
.catch(() => {
|
|
235
|
+
/* ignore — tests / environnements sans tracker */
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
2
|
+
/**
|
|
3
|
+
* Ouverture Hermes WebUI dans un onglet workspace (outil externe).
|
|
4
|
+
* URL dynamique (port libre desktop) — résolue via IPC getHermesStatus.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isRemoteDesktopClient } from "./desktop-host";
|
|
8
|
+
|
|
9
|
+
/** Partition Electron stable (catalogue outils ≥ 900000). */
|
|
10
|
+
export const HERMES_WEBUI_SITE_ID = 900099;
|
|
11
|
+
|
|
12
|
+
export const HERMES_WEBUI_TAB_TITLE = "Hermes";
|
|
13
|
+
|
|
14
|
+
export type HermesWebuiOpenTarget = {
|
|
15
|
+
url: string;
|
|
16
|
+
siteId: number;
|
|
17
|
+
title: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Statut Hermes desktop — typé soft (API marque via getShellDesktopApi). */
|
|
21
|
+
type HermesDesktopStatus = {
|
|
22
|
+
status?: string;
|
|
23
|
+
detail?: string | null;
|
|
24
|
+
uiUrl?: string | null;
|
|
25
|
+
webuiUrl?: string | null;
|
|
26
|
+
webuiStatus?: string | null;
|
|
27
|
+
bootstrapError?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function cleanDetail(value: string | null | undefined): string {
|
|
31
|
+
return String(value || "").trim().replace(/\s+/g, " ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Message actionnable fondé sur l’état réel, jamais sur l’ancien splash. */
|
|
35
|
+
export function hermesUnavailableMessage(st: HermesDesktopStatus): string {
|
|
36
|
+
const detail = cleanDetail(st.bootstrapError || st.detail);
|
|
37
|
+
if (st.status === "starting" || st.status === "installing") {
|
|
38
|
+
return "Hermes est encore en cours de démarrage. " + getShellUiBrand().productName + " continue d’attendre l’API et la WebUI.";
|
|
39
|
+
}
|
|
40
|
+
if (st.status === "missing") {
|
|
41
|
+
return `Le runtime Hermes est absent ou incomplet${detail ? ` : ${detail}` : "."} Ouvrez Configuration → Hermes → Réparer.`;
|
|
42
|
+
}
|
|
43
|
+
if (st.status === "error") {
|
|
44
|
+
const timeout = /timeout|délai/i.test(detail);
|
|
45
|
+
return `${timeout ? "Le démarrage Hermes a dépassé le délai prévu" : "Hermes n’a pas pu démarrer"}${detail ? ` : ${detail}` : "."} Vous pouvez réessayer ; les détails sont dans Configuration → Hermes → Logs.`;
|
|
46
|
+
}
|
|
47
|
+
if (st.status === "running" && st.webuiStatus !== "running") {
|
|
48
|
+
return `L’API Hermes fonctionne, mais la WebUI est ${st.webuiStatus === "missing" ? "absente" : "indisponible"}${detail ? ` : ${detail}` : "."} Utilisez Réparer dans Configuration → Hermes.`;
|
|
49
|
+
}
|
|
50
|
+
if (st.status === "stopped") {
|
|
51
|
+
return `Hermes s’est arrêté après l’ouverture de ${getShellUiBrand().productName}${detail ? ` : ${detail}` : "."} Réessayez maintenant ou consultez Configuration → Hermes → Logs.`;
|
|
52
|
+
}
|
|
53
|
+
return `Hermes est indisponible (état ${st.status}, WebUI ${st.webuiStatus})${detail ? ` : ${detail}` : "."}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Résout l’URL et relance Hermes local une fois si nécessaire. */
|
|
57
|
+
export async function resolveHermesWebuiOpenTarget(opts?: {
|
|
58
|
+
onRetry?: () => void;
|
|
59
|
+
}): Promise<
|
|
60
|
+
HermesWebuiOpenTarget | { ok: false; error: string }
|
|
61
|
+
> {
|
|
62
|
+
const api = typeof window !== "undefined" ? getShellDesktopApi() : undefined;
|
|
63
|
+
if (!api?.getHermesStatus) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: "Hermes disponible uniquement dans l’app desktop.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
let st = await api.getHermesStatus();
|
|
71
|
+
let url = (st.webuiUrl || "").trim();
|
|
72
|
+
if (
|
|
73
|
+
!url &&
|
|
74
|
+
st.status !== "skipped-remote-client" &&
|
|
75
|
+
st.status !== "remote" &&
|
|
76
|
+
api.retryHermes &&
|
|
77
|
+
// Client distant : hermes:retry est host-only, ne jamais le tenter.
|
|
78
|
+
!(await isRemoteDesktopClient())
|
|
79
|
+
) {
|
|
80
|
+
opts?.onRetry?.();
|
|
81
|
+
st = await api.retryHermes();
|
|
82
|
+
url = (st.webuiUrl || "").trim();
|
|
83
|
+
}
|
|
84
|
+
if (!url) {
|
|
85
|
+
if (st.status === "skipped-remote-client") {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error:
|
|
89
|
+
"Client distant — URL Hermes introuvable. Rejoignez un hôte via {slug}." + getShellUiBrand().publicHostSuffix + " (sous-domaine hermes.{slug}…).",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: hermesUnavailableMessage(st),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
url,
|
|
99
|
+
siteId: HERMES_WEBUI_SITE_ID,
|
|
100
|
+
title: HERMES_WEBUI_TAB_TITLE,
|
|
101
|
+
};
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: e instanceof Error ? e.message : "Statut Hermes indisponible",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isHermesWebuiOpenTarget(
|
|
111
|
+
v: HermesWebuiOpenTarget | { ok: false; error: string },
|
|
112
|
+
): v is HermesWebuiOpenTarget {
|
|
113
|
+
return Boolean(v && "url" in v && typeof v.url === "string");
|
|
114
|
+
}
|