@creezio/observability 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 +328 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/api-endpoints/http-routes.d.ts +11 -0
- package/dist/api-endpoints/http-routes.d.ts.map +1 -0
- package/dist/api-endpoints/http-routes.js +11 -0
- package/dist/api-endpoints/http-routes.js.map +1 -0
- package/dist/api-endpoints/index.d.ts +4 -0
- package/dist/api-endpoints/index.d.ts.map +1 -0
- package/dist/api-endpoints/index.js +3 -0
- package/dist/api-endpoints/index.js.map +1 -0
- package/dist/api-endpoints/registry.d.ts +66 -0
- package/dist/api-endpoints/registry.d.ts.map +1 -0
- package/dist/api-endpoints/registry.js +102 -0
- package/dist/api-endpoints/registry.js.map +1 -0
- package/dist/api-mount.d.ts +7 -0
- package/dist/api-mount.d.ts.map +1 -0
- package/dist/api-mount.js +114 -0
- package/dist/api-mount.js.map +1 -0
- package/dist/helpers.d.ts +23 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +36 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-store.d.ts +3 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +113 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/ops/emit.d.ts +8 -0
- package/dist/ops/emit.d.ts.map +1 -0
- package/dist/ops/emit.js +15 -0
- package/dist/ops/emit.js.map +1 -0
- package/dist/ops/fleet-activity.d.ts +50 -0
- package/dist/ops/fleet-activity.d.ts.map +1 -0
- package/dist/ops/fleet-activity.js +123 -0
- package/dist/ops/fleet-activity.js.map +1 -0
- package/dist/ops/fleet-agent.d.ts +102 -0
- package/dist/ops/fleet-agent.d.ts.map +1 -0
- package/dist/ops/fleet-agent.js +352 -0
- package/dist/ops/fleet-agent.js.map +1 -0
- package/dist/ops/fleet-samples.d.ts +33 -0
- package/dist/ops/fleet-samples.d.ts.map +1 -0
- package/dist/ops/fleet-samples.js +223 -0
- package/dist/ops/fleet-samples.js.map +1 -0
- package/dist/ops/journal.d.ts +56 -0
- package/dist/ops/journal.d.ts.map +1 -0
- package/dist/ops/journal.js +307 -0
- package/dist/ops/journal.js.map +1 -0
- package/dist/ops/rules.d.ts +16 -0
- package/dist/ops/rules.d.ts.map +1 -0
- package/dist/ops/rules.js +98 -0
- package/dist/ops/rules.js.map +1 -0
- package/dist/ops/types.d.ts +69 -0
- package/dist/ops/types.d.ts.map +1 -0
- package/dist/ops/types.js +119 -0
- package/dist/ops/types.js.map +1 -0
- package/dist/request-logs/config.d.ts +19 -0
- package/dist/request-logs/config.d.ts.map +1 -0
- package/dist/request-logs/config.js +36 -0
- package/dist/request-logs/config.js.map +1 -0
- package/dist/request-logs/http-routes.d.ts +8 -0
- package/dist/request-logs/http-routes.d.ts.map +1 -0
- package/dist/request-logs/http-routes.js +29 -0
- package/dist/request-logs/http-routes.js.map +1 -0
- package/dist/request-logs/index.d.ts +6 -0
- package/dist/request-logs/index.d.ts.map +1 -0
- package/dist/request-logs/index.js +5 -0
- package/dist/request-logs/index.js.map +1 -0
- package/dist/request-logs/middleware.d.ts +11 -0
- package/dist/request-logs/middleware.d.ts.map +1 -0
- package/dist/request-logs/middleware.js +198 -0
- package/dist/request-logs/middleware.js.map +1 -0
- package/dist/request-logs/request-logs.d.ts +60 -0
- package/dist/request-logs/request-logs.d.ts.map +1 -0
- package/dist/request-logs/request-logs.js +260 -0
- package/dist/request-logs/request-logs.js.map +1 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +21 -0
- package/dist/schema.js.map +1 -0
- package/dist/sqlite-driver.d.ts +16 -0
- package/dist/sqlite-driver.d.ts.map +1 -0
- package/dist/sqlite-driver.js +15 -0
- package/dist/sqlite-driver.js.map +1 -0
- package/dist/sqlite-store.d.ts +12 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +176 -0
- package/dist/sqlite-store.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/adapters.d.ts +31 -0
- package/dist/usage/adapters.d.ts.map +1 -0
- package/dist/usage/adapters.js +37 -0
- package/dist/usage/adapters.js.map +1 -0
- package/dist/usage/http-routes.d.ts +23 -0
- package/dist/usage/http-routes.d.ts.map +1 -0
- package/dist/usage/http-routes.js +150 -0
- package/dist/usage/http-routes.js.map +1 -0
- package/dist/usage/index.d.ts +11 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +7 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/usage/ui-brand.d.ts +23 -0
- package/dist/usage/ui-brand.d.ts.map +1 -0
- package/dist/usage/ui-brand.js +18 -0
- package/dist/usage/ui-brand.js.map +1 -0
- package/dist/usage/usage-analytics-productivity.d.ts +73 -0
- package/dist/usage/usage-analytics-productivity.d.ts.map +1 -0
- package/dist/usage/usage-analytics-productivity.js +317 -0
- package/dist/usage/usage-analytics-productivity.js.map +1 -0
- package/dist/usage/usage-analytics-shared.d.ts +5 -0
- package/dist/usage/usage-analytics-shared.d.ts.map +1 -0
- package/dist/usage/usage-analytics-shared.js +20 -0
- package/dist/usage/usage-analytics-shared.js.map +1 -0
- package/dist/usage/usage-analytics.d.ts +127 -0
- package/dist/usage/usage-analytics.d.ts.map +1 -0
- package/dist/usage/usage-analytics.js +338 -0
- package/dist/usage/usage-analytics.js.map +1 -0
- package/dist-cjs/api-endpoints/http-routes.js +14 -0
- package/dist-cjs/api-endpoints/http-routes.js.map +1 -0
- package/dist-cjs/api-endpoints/index.js +10 -0
- package/dist-cjs/api-endpoints/index.js.map +1 -0
- package/dist-cjs/api-endpoints/registry.js +107 -0
- package/dist-cjs/api-endpoints/registry.js.map +1 -0
- package/dist-cjs/api-mount.js +117 -0
- package/dist-cjs/api-mount.js.map +1 -0
- package/dist-cjs/helpers.js +41 -0
- package/dist-cjs/helpers.js.map +1 -0
- package/dist-cjs/index.js +118 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/memory-store.js +119 -0
- package/dist-cjs/memory-store.js.map +1 -0
- package/dist-cjs/ops/emit.js +18 -0
- package/dist-cjs/ops/emit.js.map +1 -0
- package/dist-cjs/ops/fleet-activity.js +131 -0
- package/dist-cjs/ops/fleet-activity.js.map +1 -0
- package/dist-cjs/ops/fleet-agent.js +358 -0
- package/dist-cjs/ops/fleet-agent.js.map +1 -0
- package/dist-cjs/ops/fleet-samples.js +229 -0
- package/dist-cjs/ops/fleet-samples.js.map +1 -0
- package/dist-cjs/ops/journal.js +326 -0
- package/dist-cjs/ops/journal.js.map +1 -0
- package/dist-cjs/ops/rules.js +102 -0
- package/dist-cjs/ops/rules.js.map +1 -0
- package/dist-cjs/ops/types.js +126 -0
- package/dist-cjs/ops/types.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/request-logs/config.js +42 -0
- package/dist-cjs/request-logs/config.js.map +1 -0
- package/dist-cjs/request-logs/http-routes.js +32 -0
- package/dist-cjs/request-logs/http-routes.js.map +1 -0
- package/dist-cjs/request-logs/index.js +27 -0
- package/dist-cjs/request-logs/index.js.map +1 -0
- package/dist-cjs/request-logs/middleware.js +203 -0
- package/dist-cjs/request-logs/middleware.js.map +1 -0
- package/dist-cjs/request-logs/request-logs.js +277 -0
- package/dist-cjs/request-logs/request-logs.js.map +1 -0
- package/dist-cjs/schema.js +24 -0
- package/dist-cjs/schema.js.map +1 -0
- package/dist-cjs/sqlite-driver.js +18 -0
- package/dist-cjs/sqlite-driver.js.map +1 -0
- package/dist-cjs/sqlite-store.js +182 -0
- package/dist-cjs/sqlite-store.js.map +1 -0
- package/dist-cjs/types.js +12 -0
- package/dist-cjs/types.js.map +1 -0
- package/dist-cjs/usage/adapters.js +47 -0
- package/dist-cjs/usage/adapters.js.map +1 -0
- package/dist-cjs/usage/http-routes.js +154 -0
- package/dist-cjs/usage/http-routes.js.map +1 -0
- package/dist-cjs/usage/index.js +34 -0
- package/dist-cjs/usage/index.js.map +1 -0
- package/dist-cjs/usage/ui-brand.js +23 -0
- package/dist-cjs/usage/ui-brand.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-productivity.js +321 -0
- package/dist-cjs/usage/usage-analytics-productivity.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-shared.js +23 -0
- package/dist-cjs/usage/usage-analytics-shared.js.map +1 -0
- package/dist-cjs/usage/usage-analytics.js +353 -0
- package/dist-cjs/usage/usage-analytics.js.map +1 -0
- package/fleet-collector/AGENTS.md +7 -0
- package/fleet-collector/README.md +83 -0
- package/fleet-collector/configure-fleet-npm.sh +108 -0
- package/fleet-collector/env.mjs +174 -0
- package/fleet-collector/ops-api.mjs +455 -0
- package/fleet-collector/public/admin.html +696 -0
- package/fleet-collector/public/index.html +941 -0
- package/fleet-collector/server.mjs +564 -0
- package/fleet-collector/test-fleet-collector.mjs +413 -0
- package/fleet-collector/test-server-admin.mjs +475 -0
- package/package.json +109 -0
- package/scripts/test-api-endpoints-registry.mjs +60 -0
- package/ui/analytics-client.tsx +834 -0
- package/ui/analytics-productivity-panel.tsx +513 -0
- package/ui/api-endpoints-client.tsx +228 -0
- package/ui/index.ts +39 -0
- package/ui/primitives/badge.tsx +37 -0
- package/ui/primitives/button.tsx +44 -0
- package/ui/primitives/card.tsx +44 -0
- package/ui/primitives/cn.ts +6 -0
- package/ui/primitives/input.tsx +19 -0
- package/ui/primitives/tabs.tsx +51 -0
- package/ui/request-logs-client.tsx +327 -0
- package/ui/session-usage-analytics-provider.tsx +28 -0
- package/ui/usage-analytics-client.ts +479 -0
- package/ui/usage-analytics-provider.tsx +84 -0
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="fr">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
6
|
+
<title>__FLEET_UI_TITLE__</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #faf7f1;
|
|
13
|
+
--fg: #14182f;
|
|
14
|
+
--muted: #64748b;
|
|
15
|
+
--line: #e2e8f0;
|
|
16
|
+
--card: #ffffff;
|
|
17
|
+
--sidebar: #0f172a;
|
|
18
|
+
--sidebar-hover: rgba(30,41,59,.7);
|
|
19
|
+
--sidebar-active: #1e293b;
|
|
20
|
+
--sky: #0ea5e9;
|
|
21
|
+
--sky-dark: #0284c7;
|
|
22
|
+
--ok: #059669;
|
|
23
|
+
--warn: #d97706;
|
|
24
|
+
--bad: #dc2626;
|
|
25
|
+
--radius: 10px;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { height: 100%; margin: 0; }
|
|
29
|
+
body {
|
|
30
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
31
|
+
color: var(--fg);
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
display: flex;
|
|
34
|
+
min-height: 100%;
|
|
35
|
+
}
|
|
36
|
+
button { font: inherit; cursor: pointer; }
|
|
37
|
+
a { color: inherit; text-decoration: none; }
|
|
38
|
+
|
|
39
|
+
.shell { display: flex; width: 100%; min-height: 100%; }
|
|
40
|
+
|
|
41
|
+
/* Sidebar */
|
|
42
|
+
.sidebar {
|
|
43
|
+
width: 260px; flex-shrink: 0;
|
|
44
|
+
background: var(--sidebar); color: #e2e8f0;
|
|
45
|
+
display: flex; flex-direction: column;
|
|
46
|
+
border-right: 1px solid #1e293b;
|
|
47
|
+
}
|
|
48
|
+
.brand {
|
|
49
|
+
display: flex; align-items: center; gap: 10px;
|
|
50
|
+
padding: 16px 14px; border-bottom: 1px solid #1e293b;
|
|
51
|
+
}
|
|
52
|
+
.brand-mark {
|
|
53
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
54
|
+
background: linear-gradient(135deg, #38bdf8, #0284c7);
|
|
55
|
+
color: #fff; font-size: 11px; font-weight: 700;
|
|
56
|
+
display: grid; place-items: center;
|
|
57
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.25);
|
|
58
|
+
}
|
|
59
|
+
.brand h1 { margin: 0; font-size: 14px; font-weight: 650; color: #fff; }
|
|
60
|
+
.brand p { margin: 2px 0 0; font-size: 11px; color: #94a3b8; }
|
|
61
|
+
|
|
62
|
+
.side-section { padding: 14px 12px 6px; font-size: 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #64748b; }
|
|
63
|
+
.side-nav { padding: 0 8px 12px; overflow-y: auto; flex: 1; }
|
|
64
|
+
.side-item {
|
|
65
|
+
width: 100%; text-align: left;
|
|
66
|
+
display: flex; align-items: center; gap: 10px;
|
|
67
|
+
padding: 9px 10px; margin-bottom: 2px;
|
|
68
|
+
border: 0; border-radius: 8px;
|
|
69
|
+
background: transparent; color: #cbd5e1;
|
|
70
|
+
}
|
|
71
|
+
.side-item:hover { background: var(--sidebar-hover); color: #fff; }
|
|
72
|
+
.side-item.active { background: var(--sidebar-active); color: #fff; }
|
|
73
|
+
.side-item .dot {
|
|
74
|
+
width: 8px; height: 8px; border-radius: 999px; flex-shrink: 0;
|
|
75
|
+
background: #475569;
|
|
76
|
+
}
|
|
77
|
+
.side-item .dot.online { background: #34d399; box-shadow: 0 0 0 3px rgba(52,211,153,.15); }
|
|
78
|
+
.side-item .dot.idle { background: #fbbf24; }
|
|
79
|
+
.side-item .dot.offline { background: #64748b; }
|
|
80
|
+
.side-item .meta { margin-left: auto; font-size: 11px; color: #64748b; }
|
|
81
|
+
.side-item small { display: block; font-size: 11px; color: #64748b; margin-top: 1px; }
|
|
82
|
+
.side-back {
|
|
83
|
+
margin: 8px; padding: 8px 10px; border-radius: 8px; border: 1px solid #334155;
|
|
84
|
+
background: transparent; color: #94a3b8; font-size: 12px; text-align: left;
|
|
85
|
+
}
|
|
86
|
+
.side-back:hover { background: var(--sidebar-hover); color: #fff; }
|
|
87
|
+
|
|
88
|
+
/* Main */
|
|
89
|
+
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
90
|
+
.topbar {
|
|
91
|
+
background: rgba(255,255,255,.82);
|
|
92
|
+
backdrop-filter: blur(10px);
|
|
93
|
+
border-bottom: 1px solid var(--line);
|
|
94
|
+
padding: 14px 24px 0;
|
|
95
|
+
position: sticky; top: 0; z-index: 5;
|
|
96
|
+
}
|
|
97
|
+
.crumb { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
98
|
+
.crumb button { border: 0; background: none; color: var(--sky-dark); padding: 0; }
|
|
99
|
+
.topbar h2 { margin: 0 0 2px; font-size: 20px; font-weight: 650; letter-spacing: -.02em; }
|
|
100
|
+
.topbar .sub { font-size: 13px; color: var(--muted); margin-bottom: 12px; }
|
|
101
|
+
.tabs { display: flex; gap: 4px; overflow-x: auto; }
|
|
102
|
+
.tab {
|
|
103
|
+
border: 0; background: transparent;
|
|
104
|
+
padding: 10px 14px; border-bottom: 2px solid transparent;
|
|
105
|
+
color: var(--muted); font-size: 13px; font-weight: 500; white-space: nowrap;
|
|
106
|
+
}
|
|
107
|
+
.tab:hover { color: var(--fg); }
|
|
108
|
+
.tab.active { color: var(--fg); border-bottom-color: var(--sky); }
|
|
109
|
+
|
|
110
|
+
.content { padding: 20px 24px 40px; overflow: auto; flex: 1; }
|
|
111
|
+
.grid { display: grid; gap: 14px; }
|
|
112
|
+
.grid.stats { grid-template-columns: repeat(4, minmax(0,1fr)); }
|
|
113
|
+
.grid.two { grid-template-columns: 1.2fr .8fr; }
|
|
114
|
+
@media (max-width: 1100px) {
|
|
115
|
+
.grid.stats, .grid.two { grid-template-columns: 1fr 1fr; }
|
|
116
|
+
.sidebar { width: 220px; }
|
|
117
|
+
}
|
|
118
|
+
@media (max-width: 720px) {
|
|
119
|
+
.sidebar { display: none; }
|
|
120
|
+
.grid.stats, .grid.two { grid-template-columns: 1fr; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.card {
|
|
124
|
+
background: var(--card);
|
|
125
|
+
border: 1px solid var(--line);
|
|
126
|
+
border-radius: var(--radius);
|
|
127
|
+
padding: 16px;
|
|
128
|
+
}
|
|
129
|
+
.card h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; color: var(--muted); }
|
|
130
|
+
.card .big { font-size: 28px; font-weight: 700; letter-spacing: -.03em; }
|
|
131
|
+
.card .hint { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
132
|
+
|
|
133
|
+
.pill {
|
|
134
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
135
|
+
padding: 3px 9px; border-radius: 999px;
|
|
136
|
+
font-size: 11px; font-weight: 600;
|
|
137
|
+
background: #f1f5f9; color: #334155;
|
|
138
|
+
}
|
|
139
|
+
.pill.ok { background: #ecfdf5; color: var(--ok); }
|
|
140
|
+
.pill.warn { background: #fffbeb; color: var(--warn); }
|
|
141
|
+
.pill.bad { background: #fef2f2; color: var(--bad); }
|
|
142
|
+
.pill.sky { background: #e0f2fe; color: var(--sky-dark); }
|
|
143
|
+
|
|
144
|
+
.list { display: flex; flex-direction: column; gap: 8px; }
|
|
145
|
+
.row {
|
|
146
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
147
|
+
padding: 12px 14px; border: 1px solid var(--line);
|
|
148
|
+
border-radius: var(--radius); background: #fff;
|
|
149
|
+
text-align: left; width: 100%;
|
|
150
|
+
}
|
|
151
|
+
.row:hover { border-color: #cbd5e1; box-shadow: 0 1px 2px rgba(15,23,42,.04); }
|
|
152
|
+
.row .avatar {
|
|
153
|
+
width: 36px; height: 36px; border-radius: 999px; flex-shrink: 0;
|
|
154
|
+
background: #e0f2fe; color: var(--sky-dark);
|
|
155
|
+
display: grid; place-items: center; font-size: 12px; font-weight: 700;
|
|
156
|
+
}
|
|
157
|
+
.row .body { flex: 1; min-width: 0; }
|
|
158
|
+
.row .title { font-size: 14px; font-weight: 600; }
|
|
159
|
+
.row .desc { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
160
|
+
.row .time { font-size: 11px; color: #94a3b8; white-space: nowrap; }
|
|
161
|
+
|
|
162
|
+
.timeline .row { border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; }
|
|
163
|
+
.timeline .row:hover { background: #fdfcfa; }
|
|
164
|
+
|
|
165
|
+
.chat {
|
|
166
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
167
|
+
max-height: 520px; overflow: auto;
|
|
168
|
+
padding: 12px; background: #f8fafc; border-radius: var(--radius);
|
|
169
|
+
border: 1px solid var(--line);
|
|
170
|
+
}
|
|
171
|
+
.bubble {
|
|
172
|
+
max-width: 85%; padding: 10px 12px; border-radius: 12px;
|
|
173
|
+
font-size: 13px; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
|
|
174
|
+
}
|
|
175
|
+
.bubble.user { align-self: flex-end; background: #e0f2fe; color: #0c4a6e; }
|
|
176
|
+
.bubble.assistant { align-self: flex-start; background: #fff; border: 1px solid var(--line); }
|
|
177
|
+
.bubble .who { font-size: 10px; font-weight: 600; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; }
|
|
178
|
+
|
|
179
|
+
.health-grid { display: grid; grid-template-columns: repeat(5,1fr); gap: 8px; }
|
|
180
|
+
.health-item {
|
|
181
|
+
padding: 12px; border-radius: 8px; background: #f8fafc; border: 1px solid var(--line);
|
|
182
|
+
text-align: center;
|
|
183
|
+
}
|
|
184
|
+
.health-item strong { display: block; font-size: 12px; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
|
|
185
|
+
|
|
186
|
+
.btn {
|
|
187
|
+
border: 0; border-radius: 8px; padding: 8px 12px;
|
|
188
|
+
background: var(--sky); color: #fff; font-weight: 600; font-size: 13px;
|
|
189
|
+
}
|
|
190
|
+
.btn:hover { background: var(--sky-dark); }
|
|
191
|
+
.btn.ghost { background: #fff; color: var(--fg); border: 1px solid var(--line); }
|
|
192
|
+
.btn.ghost:hover { background: #f8fafc; }
|
|
193
|
+
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
194
|
+
|
|
195
|
+
.empty {
|
|
196
|
+
padding: 40px 20px; text-align: center; color: var(--muted); font-size: 14px;
|
|
197
|
+
border: 1px dashed var(--line); border-radius: var(--radius); background: #fff;
|
|
198
|
+
}
|
|
199
|
+
.hidden { display: none !important; }
|
|
200
|
+
select {
|
|
201
|
+
border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;
|
|
202
|
+
background: #fff; font: inherit;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
205
|
+
</head>
|
|
206
|
+
<body>
|
|
207
|
+
<div class="shell">
|
|
208
|
+
<aside class="sidebar">
|
|
209
|
+
<div class="brand">
|
|
210
|
+
<div class="brand-mark" id="brandMark">__FLEET_UI_MARK__</div>
|
|
211
|
+
<div>
|
|
212
|
+
<h1 id="brandTitle">__FLEET_UI_TITLE__</h1>
|
|
213
|
+
<p id="sideStats">Support clients</p>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
<button class="side-back hidden" id="btnBack" type="button">← Tous les serveurs</button>
|
|
217
|
+
<div class="side-section" id="sideLabel">Serveurs</div>
|
|
218
|
+
<nav class="side-nav" id="sideNav"></nav>
|
|
219
|
+
</aside>
|
|
220
|
+
|
|
221
|
+
<div class="main">
|
|
222
|
+
<header class="topbar">
|
|
223
|
+
<div class="crumb" id="crumb"></div>
|
|
224
|
+
<h2 id="pageTitle">Flotte</h2>
|
|
225
|
+
<div class="sub" id="pageSub">Choisissez un serveur pour voir l’activité</div>
|
|
226
|
+
<div class="tabs" id="tabs"></div>
|
|
227
|
+
</header>
|
|
228
|
+
<main class="content" id="content"></main>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
const state = {
|
|
234
|
+
fleet: null,
|
|
235
|
+
server: null,
|
|
236
|
+
user: null,
|
|
237
|
+
route: { slug: null, userId: null, tab: "overview" },
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const $ = (id) => document.getElementById(id);
|
|
241
|
+
|
|
242
|
+
async function api(path, opts = {}) {
|
|
243
|
+
const r = await fetch(path, { credentials: "same-origin", ...opts, headers: {
|
|
244
|
+
...(opts.body ? { "Content-Type": "application/json" } : {}),
|
|
245
|
+
...(opts.headers || {}),
|
|
246
|
+
}});
|
|
247
|
+
if (r.status === 401) {
|
|
248
|
+
location.reload();
|
|
249
|
+
throw new Error("auth");
|
|
250
|
+
}
|
|
251
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
252
|
+
return r.json();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ago(iso) {
|
|
256
|
+
if (!iso) return "—";
|
|
257
|
+
const s = (Date.now() - new Date(iso).getTime()) / 1000;
|
|
258
|
+
if (s < 90) return "à l’instant";
|
|
259
|
+
if (s < 3600) return Math.floor(s / 60) + " min";
|
|
260
|
+
if (s < 86400) return Math.floor(s / 3600) + " h";
|
|
261
|
+
return Math.floor(s / 86400) + " j";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function pillOnline(v) {
|
|
265
|
+
const cls = v === "online" ? "ok" : v === "idle" ? "warn" : "bad";
|
|
266
|
+
const label = v === "online" ? "En ligne" : v === "idle" ? "Inactif" : "Hors ligne";
|
|
267
|
+
return `<span class="pill ${cls}">${label}</span>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function initials(name) {
|
|
271
|
+
return String(name || "?")
|
|
272
|
+
.split(/[\s.@_-]+/)
|
|
273
|
+
.filter(Boolean)
|
|
274
|
+
.slice(0, 2)
|
|
275
|
+
.map((x) => x[0]?.toUpperCase() || "")
|
|
276
|
+
.join("") || "?";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseHash() {
|
|
280
|
+
const h = (location.hash || "#/").replace(/^#/, "");
|
|
281
|
+
const parts = h.split("/").filter(Boolean);
|
|
282
|
+
// /s/:slug[/u/:userId][/t/:tab]
|
|
283
|
+
let slug = null, userId = null, tab = "overview";
|
|
284
|
+
for (let i = 0; i < parts.length; i++) {
|
|
285
|
+
if (parts[i] === "s" && parts[i + 1]) { slug = decodeURIComponent(parts[++i]); continue; }
|
|
286
|
+
if (parts[i] === "u" && parts[i + 1]) { userId = decodeURIComponent(parts[++i]); continue; }
|
|
287
|
+
if (parts[i] === "t" && parts[i + 1]) { tab = decodeURIComponent(parts[++i]); continue; }
|
|
288
|
+
}
|
|
289
|
+
state.route = { slug, userId, tab };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function go(slug, userId, tab) {
|
|
293
|
+
let hash = "#/";
|
|
294
|
+
if (slug) {
|
|
295
|
+
hash = `#/s/${encodeURIComponent(slug)}`;
|
|
296
|
+
if (userId) hash += `/u/${encodeURIComponent(userId)}`;
|
|
297
|
+
if (tab && tab !== "overview") hash += `/t/${encodeURIComponent(tab)}`;
|
|
298
|
+
else if (tab === "overview" && !userId) { /* keep */ }
|
|
299
|
+
else if (userId && tab) hash = `#/s/${encodeURIComponent(slug)}/u/${encodeURIComponent(userId)}/t/${encodeURIComponent(tab)}`;
|
|
300
|
+
}
|
|
301
|
+
if (location.hash !== hash) location.hash = hash;
|
|
302
|
+
else void render();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function setTabs(items, active) {
|
|
306
|
+
$("tabs").innerHTML = items.map((t) =>
|
|
307
|
+
`<button type="button" class="tab ${t.id === active ? "active" : ""}" data-tab="${t.id}">${t.label}</button>`
|
|
308
|
+
).join("");
|
|
309
|
+
$("tabs").querySelectorAll(".tab").forEach((btn) => {
|
|
310
|
+
btn.onclick = () => go(state.route.slug, state.route.userId, btn.dataset.tab);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderSidebar() {
|
|
315
|
+
const { slug, userId } = state.route;
|
|
316
|
+
const nav = $("sideNav");
|
|
317
|
+
const back = $("btnBack");
|
|
318
|
+
if (!slug) {
|
|
319
|
+
back.classList.add("hidden");
|
|
320
|
+
$("sideLabel").textContent = "Serveurs";
|
|
321
|
+
const servers = state.fleet?.servers || [];
|
|
322
|
+
nav.innerHTML = servers.map((s) => `
|
|
323
|
+
<button type="button" class="side-item" data-slug="${s.slug}">
|
|
324
|
+
<span class="dot ${s.online}"></span>
|
|
325
|
+
<span>
|
|
326
|
+
<strong>${s.slug}</strong>
|
|
327
|
+
<small>${s.appVersion} · ${s.userCount} util.</small>
|
|
328
|
+
</span>
|
|
329
|
+
<span class="meta">${ago(s.lastSeen)}</span>
|
|
330
|
+
</button>
|
|
331
|
+
`).join("") || `<div style="padding:12px;color:#64748b;font-size:12px">Aucun serveur</div>`;
|
|
332
|
+
nav.querySelectorAll("[data-slug]").forEach((btn) => {
|
|
333
|
+
btn.onclick = () => go(btn.dataset.slug, null, "overview");
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
back.classList.remove("hidden");
|
|
338
|
+
back.onclick = () => go(null, null, "overview");
|
|
339
|
+
$("sideLabel").textContent = "Utilisateurs · " + slug;
|
|
340
|
+
const users = state.server?.users || [];
|
|
341
|
+
nav.innerHTML = users.map((u) => `
|
|
342
|
+
<button type="button" class="side-item ${userId === u.id ? "active" : ""}" data-uid="${u.id}">
|
|
343
|
+
<span class="dot ${u.online}"></span>
|
|
344
|
+
<span>
|
|
345
|
+
<strong>${u.username}</strong>
|
|
346
|
+
<small>${u.role}${u.kind === "ai" ? " · IA" : ""}</small>
|
|
347
|
+
</span>
|
|
348
|
+
</button>
|
|
349
|
+
`).join("") || `<div style="padding:12px;color:#64748b;font-size:12px">Aucun utilisateur</div>`;
|
|
350
|
+
nav.querySelectorAll("[data-uid]").forEach((btn) => {
|
|
351
|
+
btn.onclick = () => go(slug, btn.dataset.uid, "activity");
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function healthLabel(v) {
|
|
356
|
+
const s = String(v || "—");
|
|
357
|
+
if (s === "running") return pillOnline("online").replace("En ligne", "OK");
|
|
358
|
+
if (!v) return `<span class="pill">—</span>`;
|
|
359
|
+
return `<span class="pill warn">${s}</span>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function brand() {
|
|
363
|
+
return window.__FLEET_BRAND__ || {
|
|
364
|
+
title: "Fleet",
|
|
365
|
+
mark: "FL",
|
|
366
|
+
homeTitle: "Flotte",
|
|
367
|
+
extrasTitle: "Dossiers",
|
|
368
|
+
etatLabels: {},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function etatPills(parEtat) {
|
|
373
|
+
const labels = brand().etatLabels || {};
|
|
374
|
+
const entries = Object.entries(parEtat || {}).filter(([, n]) => n > 0);
|
|
375
|
+
if (!entries.length) return "";
|
|
376
|
+
const cls = { VALIDE: "ok", EN_COURS: "sky", CONTROLES_OK: "warn" };
|
|
377
|
+
return entries
|
|
378
|
+
.map(([e, n]) => `<span class="pill ${cls[e] || ""}">${esc(labels[e] || e)} ${n}</span>`)
|
|
379
|
+
.join(" ");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function dossiersDesc(d) {
|
|
383
|
+
if (!d || typeof d !== "object") return "";
|
|
384
|
+
const total = Number(d.total) || 0;
|
|
385
|
+
const pieces = d.pieces && d.pieces.total
|
|
386
|
+
? ` · pièces ${d.pieces.faites}/${d.pieces.total}`
|
|
387
|
+
: "";
|
|
388
|
+
return ` · ${total} dossier(s)${pieces}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function renderHome() {
|
|
392
|
+
const f = state.fleet;
|
|
393
|
+
const b = brand();
|
|
394
|
+
$("pageTitle").textContent = b.homeTitle || "Flotte";
|
|
395
|
+
$("pageSub").textContent = "Sélectionnez un serveur (slug) dans la barre latérale";
|
|
396
|
+
$("crumb").textContent = "Support";
|
|
397
|
+
setTabs([], "overview");
|
|
398
|
+
const servers = f?.servers || [];
|
|
399
|
+
const dossiers = f?.stats?.dossiers || { total: 0, parEtat: {}, actifs7j: 0 };
|
|
400
|
+
const versions = Object.entries(f?.stats?.versions || {}).sort((a, b) => b[1] - a[1]);
|
|
401
|
+
const hasExtras = dossiers.total > 0 || Object.keys(dossiers.parEtat || {}).length > 0;
|
|
402
|
+
let crashes = [];
|
|
403
|
+
try {
|
|
404
|
+
crashes = (await api("/ops/api/crashes")).crashes || [];
|
|
405
|
+
} catch { /* section optionnelle */ }
|
|
406
|
+
$("content").innerHTML = `
|
|
407
|
+
<div class="grid stats">
|
|
408
|
+
<div class="card"><h3>Serveurs</h3><div class="big">${f?.stats?.servers ?? 0}</div><div class="hint">${f?.stats?.online ?? 0} en ligne</div></div>
|
|
409
|
+
${hasExtras
|
|
410
|
+
? `<div class="card"><h3>${esc(b.extrasTitle || "Dossiers")}</h3><div class="big">${dossiers.total}</div><div class="hint">${dossiers.actifs7j} actif(s) sur 7 j</div></div>`
|
|
411
|
+
: `<div class="card"><h3>Utilisateurs</h3><div class="big">${f?.stats?.users ?? 0}</div><div class="hint">comptes connus</div></div>`}
|
|
412
|
+
<div class="card"><h3>Incidents</h3><div class="big">${f?.stats?.crashes ?? 0}</div><div class="hint">crashes cumulés</div></div>
|
|
413
|
+
<div class="card"><h3>Versions</h3>
|
|
414
|
+
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px">
|
|
415
|
+
${versions.map(([v, n]) => `<span class="pill sky">v${esc(v)} × ${n}</span>`).join("") || '<span class="pill">—</span>'}
|
|
416
|
+
</div>
|
|
417
|
+
<div class="hint">répartition flotte (avant rollout)</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
${Object.keys(dossiers.parEtat || {}).length ? `
|
|
421
|
+
<div style="height:12px"></div>
|
|
422
|
+
<div class="card">
|
|
423
|
+
<h3>${esc(b.extrasTitle || "Dossiers")} par état (flotte)</h3>
|
|
424
|
+
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px">${etatPills(dossiers.parEtat)}</div>
|
|
425
|
+
</div>
|
|
426
|
+
` : ""}
|
|
427
|
+
<div style="height:16px"></div>
|
|
428
|
+
<div class="list">
|
|
429
|
+
${servers.map((s) => `
|
|
430
|
+
<button type="button" class="row" data-open="${s.slug}">
|
|
431
|
+
<div class="avatar">${initials(s.slug)}</div>
|
|
432
|
+
<div class="body">
|
|
433
|
+
<div class="title">${s.slug} ${pillOnline(s.online)}</div>
|
|
434
|
+
<div class="desc">${s.hostname} · v${s.appVersion} · ${s.platform} · ${s.userCount} utilisateur(s)${dossiersDesc(s.dossierStats)}</div>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="time">${ago(s.lastSeen)}</div>
|
|
437
|
+
</button>
|
|
438
|
+
`).join("") || `<div class="empty">Aucun serveur n’a encore envoyé de télémétrie.<br/>Activez Support dans Configuration → Support sur un client.</div>`}
|
|
439
|
+
</div>
|
|
440
|
+
${crashes.length ? `
|
|
441
|
+
<div style="height:16px"></div>
|
|
442
|
+
<div class="card">
|
|
443
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">Derniers incidents (flotte)</h3>
|
|
444
|
+
<div class="list timeline">
|
|
445
|
+
${crashes.slice(0, 10).map((c) => `
|
|
446
|
+
<div class="row">
|
|
447
|
+
<div class="avatar" style="background:#fef2f2;color:var(--bad)">!</div>
|
|
448
|
+
<div class="body">
|
|
449
|
+
<div class="title" style="font-size:13px">${esc(c.kind || "incident")} <span class="pill">v${esc(c.appVersion || "?")}</span> ${c.bootStage ? `<span class="pill">${esc(c.bootStage)}</span>` : ""}</div>
|
|
450
|
+
<div class="desc">${esc(c.message || "Sans détail")} · install <code>${esc(String(c.installId || "").slice(0, 8))}</code></div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="time">${ago(c.timestamp)}</div>
|
|
453
|
+
</div>
|
|
454
|
+
`).join("")}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
` : ""}
|
|
458
|
+
`;
|
|
459
|
+
$("content").querySelectorAll("[data-open]").forEach((btn) => {
|
|
460
|
+
btn.onclick = () => go(btn.dataset.open, null, "overview");
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderServerOverview() {
|
|
465
|
+
const s = state.server.server;
|
|
466
|
+
const users = state.server.users || [];
|
|
467
|
+
const activity = (state.server.activity || []).slice(0, 12);
|
|
468
|
+
const d = s.dossierStats;
|
|
469
|
+
const b = brand();
|
|
470
|
+
const third = d
|
|
471
|
+
? `<div class="card"><h3>${esc(b.extrasTitle || "Dossiers")}</h3><div class="big">${Number(d.total) || 0}</div><div class="hint">${Number(d.actifs7j) || 0} actif(s) 7 j${d.pieces && d.pieces.total ? ` · pièces ${d.pieces.faites}/${d.pieces.total}` : ""}</div></div>`
|
|
472
|
+
: `<div class="card"><h3>Utilisateurs</h3><div class="big">${users.length}</div><div class="hint">${users.filter(u=>u.online==="online").length} en ligne</div></div>`;
|
|
473
|
+
$("content").innerHTML = `
|
|
474
|
+
<div class="grid stats">
|
|
475
|
+
<div class="card"><h3>Statut</h3><div style="margin-top:8px">${pillOnline(s.online)}</div><div class="hint">vu ${ago(s.lastSeen)}</div></div>
|
|
476
|
+
<div class="card"><h3>Version</h3><div class="big" style="font-size:22px">${s.appVersion || "?"}</div><div class="hint">${s.platform}/${s.arch}</div></div>
|
|
477
|
+
${third}
|
|
478
|
+
<div class="card"><h3>Incidents</h3><div class="big">${s.crashCount || 0}</div><div class="hint">machine ${s.machine || "—"}</div></div>
|
|
479
|
+
</div>
|
|
480
|
+
${d && Object.keys(d.parEtat || {}).length ? `
|
|
481
|
+
<div style="height:12px"></div>
|
|
482
|
+
<div class="card">
|
|
483
|
+
<h3>${esc(b.extrasTitle || "Dossiers")} par état</h3>
|
|
484
|
+
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px">${etatPills(d.parEtat)}</div>
|
|
485
|
+
</div>
|
|
486
|
+
` : ""}
|
|
487
|
+
<div style="height:16px"></div>
|
|
488
|
+
<div class="grid two">
|
|
489
|
+
<div class="card">
|
|
490
|
+
<h3 style="margin-bottom:12px;color:var(--fg);font-size:15px">Utilisateurs</h3>
|
|
491
|
+
<div class="list">
|
|
492
|
+
${users.map((u) => `
|
|
493
|
+
<button type="button" class="row" data-uid="${u.id}">
|
|
494
|
+
<div class="avatar">${initials(u.username)}</div>
|
|
495
|
+
<div class="body">
|
|
496
|
+
<div class="title">${u.username} ${pillOnline(u.online)}</div>
|
|
497
|
+
<div class="desc">${u.role}${u.kind === "ai" ? " · agent IA" : ""}</div>
|
|
498
|
+
</div>
|
|
499
|
+
</button>
|
|
500
|
+
`).join("")}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="card">
|
|
504
|
+
<h3 style="margin-bottom:12px;color:var(--fg);font-size:15px">Activité récente</h3>
|
|
505
|
+
<div class="list timeline">
|
|
506
|
+
${activity.map((a) => `
|
|
507
|
+
<div class="row">
|
|
508
|
+
<div class="body">
|
|
509
|
+
<div class="title" style="font-size:13px;font-weight:500">${esc(a.label)}</div>
|
|
510
|
+
<div class="desc"><span class="pill sky">${a.type}</span> ${a.path ? esc(a.path) : ""}</div>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="time">${ago(a.at)}</div>
|
|
513
|
+
</div>
|
|
514
|
+
`).join("") || `<div class="empty">Pas encore d’activité remontée</div>`}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
`;
|
|
519
|
+
$("content").querySelectorAll("[data-uid]").forEach((btn) => {
|
|
520
|
+
btn.onclick = () => go(state.route.slug, btn.dataset.uid, "activity");
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function renderUsers() {
|
|
525
|
+
const users = state.server.users || [];
|
|
526
|
+
$("content").innerHTML = `
|
|
527
|
+
<div class="list">
|
|
528
|
+
${users.map((u) => `
|
|
529
|
+
<button type="button" class="row" data-uid="${u.id}">
|
|
530
|
+
<div class="avatar">${initials(u.username)}</div>
|
|
531
|
+
<div class="body">
|
|
532
|
+
<div class="title">${u.username} ${pillOnline(u.online)}</div>
|
|
533
|
+
<div class="desc">Rôle ${u.role} · vu ${ago(u.lastSeen)}</div>
|
|
534
|
+
</div>
|
|
535
|
+
<span class="btn ghost">Ouvrir</span>
|
|
536
|
+
</button>
|
|
537
|
+
`).join("") || `<div class="empty">Aucun utilisateur — activez le scope « Comptes locaux » sur le client.</div>`}
|
|
538
|
+
</div>
|
|
539
|
+
`;
|
|
540
|
+
$("content").querySelectorAll("[data-uid]").forEach((btn) => {
|
|
541
|
+
btn.onclick = () => go(state.route.slug, btn.dataset.uid, "activity");
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function fmtDur(ms) {
|
|
546
|
+
if (ms == null || !Number.isFinite(ms)) return "";
|
|
547
|
+
if (ms < 1000) return ms + " ms";
|
|
548
|
+
if (ms < 60000) return Math.round(ms / 1000) + " s";
|
|
549
|
+
return Math.round(ms / 60000) + " min";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function renderActivity(list) {
|
|
553
|
+
$("content").innerHTML = `
|
|
554
|
+
<div class="card">
|
|
555
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">Fil d’activité</h3>
|
|
556
|
+
<div class="list timeline">
|
|
557
|
+
${(list || []).map((a) => `
|
|
558
|
+
<div class="row">
|
|
559
|
+
<div class="avatar" style="width:28px;height:28px;font-size:10px">${initials(a.name || a.type)}</div>
|
|
560
|
+
<div class="body">
|
|
561
|
+
<div class="title" style="font-size:13px;font-weight:500">${esc(a.label)}</div>
|
|
562
|
+
<div class="desc">
|
|
563
|
+
<span class="pill sky">${esc(a.name || a.type)}</span>
|
|
564
|
+
${a.category ? `<span class="pill">${esc(a.category)}</span>` : ""}
|
|
565
|
+
${a.durationMs != null ? `<span class="pill ok">${fmtDur(a.durationMs)}</span>` : ""}
|
|
566
|
+
${a.path ? `<code style="font-size:11px">${esc(a.path)}</code>` : ""}
|
|
567
|
+
${a.username ? ` · ${esc(a.username)}` : ""}
|
|
568
|
+
${a.meta && (a.meta.query || a.meta.aid) ? ` · <span class="muted">${esc(a.meta.query || a.meta.aid || "")}</span>` : ""}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="time">${ago(a.at)}<br/><span style="font-size:10px">${esc(a.at || "")}</span></div>
|
|
572
|
+
</div>
|
|
573
|
+
`).join("") || `<div class="empty">Aucune activité pour l’instant.<br/>Sur le desktop : Support → Journal d’actions ON, puis naviguez / cliquez. Les pages seules peuvent apparaître avant les clics (desktop 0.6.2+).</div>`}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderHeatmap(rows) {
|
|
580
|
+
const list = rows || [];
|
|
581
|
+
$("content").innerHTML = `
|
|
582
|
+
<div class="card">
|
|
583
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">Temps passé par page</h3>
|
|
584
|
+
<div class="list">
|
|
585
|
+
${list.map((r) => `
|
|
586
|
+
<div class="row">
|
|
587
|
+
<div class="body">
|
|
588
|
+
<div class="title" style="font-size:13px"><code>${esc(r.path)}</code></div>
|
|
589
|
+
<div class="desc">${r.views} vue(s) · séjour cumulé ${fmtDur(r.dwellMs)}</div>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="time">${fmtDur(r.dwellMs)}</div>
|
|
592
|
+
</div>
|
|
593
|
+
`).join("") || `<div class="empty">Pas encore de durées (page.hide). Naviguez dans l’app puis attendez le sync.</div>`}
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function renderPlugins() {
|
|
600
|
+
const plugins = state.server?.server?.plugins || [];
|
|
601
|
+
$("content").innerHTML = `
|
|
602
|
+
<div class="list">
|
|
603
|
+
${plugins.map((p) => `
|
|
604
|
+
<div class="row">
|
|
605
|
+
<div class="body">
|
|
606
|
+
<div class="title">${esc(p.name || p.id)} ${p.enabled ? pillOnline("online").replace("En ligne","ON") : '<span class="pill">OFF</span>'}</div>
|
|
607
|
+
<div class="desc">${esc(p.id)} · v${esc(p.version || "?")}</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
`).join("") || `<div class="empty">Aucun plugin — activez le scope Plugins sur le client.</div>`}
|
|
611
|
+
</div>
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function renderConsent() {
|
|
616
|
+
const c = state.server?.server?.consent;
|
|
617
|
+
if (!c) {
|
|
618
|
+
$("content").innerHTML = `<div class="empty">Pas de consentement remonté</div>`;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const scopes = c.scopes || {};
|
|
622
|
+
$("content").innerHTML = `
|
|
623
|
+
<div class="card">
|
|
624
|
+
<h3 style="color:var(--fg);font-size:15px">Consentement Support</h3>
|
|
625
|
+
<p class="hint">Master : ${c.enabled ? pillOnline("online").replace("En ligne","ON") : '<span class="pill bad">OFF</span>'}
|
|
626
|
+
${c.consentAt ? ` · depuis ${esc(c.consentAt)}` : ""}</p>
|
|
627
|
+
<div class="list" style="margin-top:12px">
|
|
628
|
+
${Object.entries(scopes).map(([k,v]) => `
|
|
629
|
+
<div class="row">
|
|
630
|
+
<div class="body"><div class="title" style="font-size:13px">${esc(k)}</div></div>
|
|
631
|
+
<div>${v ? '<span class="pill ok">ON</span>' : '<span class="pill">OFF</span>'}</div>
|
|
632
|
+
</div>
|
|
633
|
+
`).join("")}
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function renderConversations(threads) {
|
|
640
|
+
const list = threads || [];
|
|
641
|
+
if (!list.length) {
|
|
642
|
+
$("content").innerHTML = `<div class="empty">Aucune conversation remontée.<br/>Activez « Hermes — chats » et/ou « Assistant in-app » dans Support.</div>`;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
let selected = list[0].id;
|
|
646
|
+
const paint = () => {
|
|
647
|
+
const t = list.find((x) => x.id === selected) || list[0];
|
|
648
|
+
$("content").innerHTML = `
|
|
649
|
+
<div class="grid two">
|
|
650
|
+
<div class="card" style="padding:10px">
|
|
651
|
+
<h3 style="color:var(--fg);font-size:15px;margin:6px 10px 12px">Conversations</h3>
|
|
652
|
+
<div class="list">
|
|
653
|
+
${list.map((c) => `
|
|
654
|
+
<button type="button" class="row" data-cid="${escAttr(c.id)}" style="${c.id===selected?"border-color:var(--sky);background:#f0f9ff":""}">
|
|
655
|
+
<div class="body">
|
|
656
|
+
<div class="title" style="font-size:13px">${esc(c.title)} <span class="pill">${c.source}</span></div>
|
|
657
|
+
<div class="desc">${esc(c.preview || "")}</div>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="time">${ago(c.updatedAt)}</div>
|
|
660
|
+
</button>
|
|
661
|
+
`).join("")}
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
<div class="card">
|
|
665
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">${esc(t.title)}</h3>
|
|
666
|
+
<div class="chat">
|
|
667
|
+
${(t.messages || []).map((m) => `
|
|
668
|
+
<div class="bubble ${m.role === "user" ? "user" : "assistant"}">
|
|
669
|
+
<div class="who">${m.role === "user" ? "Utilisateur" : "Assistant"} · ${ago(m.at)}</div>
|
|
670
|
+
${esc(m.content || "")}
|
|
671
|
+
</div>
|
|
672
|
+
`).join("") || `<div class="empty">Pas de messages</div>`}
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
`;
|
|
677
|
+
$("content").querySelectorAll("[data-cid]").forEach((btn) => {
|
|
678
|
+
btn.onclick = () => { selected = btn.dataset.cid; paint(); };
|
|
679
|
+
});
|
|
680
|
+
};
|
|
681
|
+
paint();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function renderHealth() {
|
|
685
|
+
const h = state.server.server.health || {};
|
|
686
|
+
const keys = [
|
|
687
|
+
["next", "CRM"],
|
|
688
|
+
["meili", "Recherche"],
|
|
689
|
+
["hermes", "Hermes"],
|
|
690
|
+
["n8n", "n8n"],
|
|
691
|
+
["tunnel", "Tunnel"],
|
|
692
|
+
];
|
|
693
|
+
$("content").innerHTML = `
|
|
694
|
+
<div class="card">
|
|
695
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:14px">Santé des services</h3>
|
|
696
|
+
<div class="health-grid">
|
|
697
|
+
${keys.map(([k, label]) => `
|
|
698
|
+
<div class="health-item">
|
|
699
|
+
<strong>${label}</strong>
|
|
700
|
+
${healthLabel(h[k])}
|
|
701
|
+
</div>
|
|
702
|
+
`).join("")}
|
|
703
|
+
</div>
|
|
704
|
+
<div style="margin-top:16px;font-size:13px;color:var(--muted)">
|
|
705
|
+
Machine : <strong>${esc(state.server.server.machine || "—")}</strong>
|
|
706
|
+
· OS ${esc(state.server.server.osRelease || "")}
|
|
707
|
+
· install <code>${esc((state.server.server.installId || "").slice(0, 8))}</code>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function renderIncidents() {
|
|
714
|
+
const items = state.server.incidents || [];
|
|
715
|
+
$("content").innerHTML = `
|
|
716
|
+
<div class="list">
|
|
717
|
+
${items.map((c) => `
|
|
718
|
+
<div class="row">
|
|
719
|
+
<div class="avatar" style="background:#fef2f2;color:var(--bad)">!</div>
|
|
720
|
+
<div class="body">
|
|
721
|
+
<div class="title">${esc(c.kind || "incident")} ${c.bootStage ? `<span class="pill">${esc(c.bootStage)}</span>` : ""}</div>
|
|
722
|
+
<div class="desc">${esc(c.message || "Sans détail")}</div>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="time">${ago(c.at)}</div>
|
|
725
|
+
</div>
|
|
726
|
+
`).join("") || `<div class="empty">Aucun incident pour ce serveur</div>`}
|
|
727
|
+
</div>
|
|
728
|
+
`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function opsLevelPill(level) {
|
|
732
|
+
const colors = {
|
|
733
|
+
decision: "background:#eef2ff;color:#4338ca",
|
|
734
|
+
event: "background:#f0fdf4;color:#15803d",
|
|
735
|
+
anomaly: "background:#fff7ed;color:#c2410c",
|
|
736
|
+
error: "background:#fef2f2;color:#b91c1c",
|
|
737
|
+
crash: "background:#fef2f2;color:#7f1d1d",
|
|
738
|
+
};
|
|
739
|
+
return `<span class="pill" style="${colors[level] || ""}">${esc(level || "?")}</span>`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function renderOps() {
|
|
743
|
+
const ops = state.server.ops || {};
|
|
744
|
+
const summary = ops.lastBootSummary || null;
|
|
745
|
+
const events = ops.events || [];
|
|
746
|
+
const decisions = summary ? Object.entries(summary.decisions || {}) : [];
|
|
747
|
+
const durations = summary ? Object.entries(summary.durations || {}) : [];
|
|
748
|
+
$("content").innerHTML = `
|
|
749
|
+
<div class="card">
|
|
750
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:10px">Dernier boot</h3>
|
|
751
|
+
${summary ? `
|
|
752
|
+
<div class="hint" style="margin-bottom:10px">
|
|
753
|
+
boot <code>${esc(String(summary.bootId || "").slice(0, 8))}</code>
|
|
754
|
+
· ${esc(summary.startedAt || "?")}
|
|
755
|
+
${summary.durationMs ? ` · ${Math.round(summary.durationMs / 1000)}s` : ""}
|
|
756
|
+
· v${esc(summary.appVersion || "?")}
|
|
757
|
+
</div>
|
|
758
|
+
<div class="list">
|
|
759
|
+
${decisions.map(([kind, d]) => `
|
|
760
|
+
<div class="row">
|
|
761
|
+
<div class="body">
|
|
762
|
+
<div class="title" style="font-size:13px">${esc(kind)} → <strong>${esc(d.outcome || "?")}</strong></div>
|
|
763
|
+
<div class="desc">${esc(d.reason || "")}</div>
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
`).join("") || `<div class="empty">Aucune décision enregistrée</div>`}
|
|
767
|
+
</div>
|
|
768
|
+
${durations.length ? `
|
|
769
|
+
<div class="hint" style="margin-top:10px">
|
|
770
|
+
Durées : ${durations.map(([k, v]) => `${esc(k)} ${Math.round(v / 1000)}s`).join(" · ")}
|
|
771
|
+
</div>
|
|
772
|
+
` : ""}
|
|
773
|
+
` : `<div class="empty">Pas encore de résumé de boot (client < 0.9 ou scope « Boîte noire » désactivé)</div>`}
|
|
774
|
+
</div>
|
|
775
|
+
<div style="height:16px"></div>
|
|
776
|
+
<div class="card">
|
|
777
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">Événements (${events.length})</h3>
|
|
778
|
+
<div class="list timeline">
|
|
779
|
+
${events.map((e) => `
|
|
780
|
+
<div class="row">
|
|
781
|
+
<div class="body">
|
|
782
|
+
<div class="title" style="font-size:13px">
|
|
783
|
+
${opsLevelPill(e.level)} ${esc(e.kind)}
|
|
784
|
+
${e.outcome ? `→ <strong>${esc(e.outcome)}</strong>` : ""}
|
|
785
|
+
${e.durationMs != null ? `<span class="pill">${Math.round(e.durationMs / 1000)}s</span>` : ""}
|
|
786
|
+
</div>
|
|
787
|
+
<div class="desc">${esc(e.reason || "")}${e.ctx ? ` · <code style="font-size:11px">${esc(JSON.stringify(e.ctx).slice(0, 220))}</code>` : ""}</div>
|
|
788
|
+
</div>
|
|
789
|
+
<div class="time">${ago(e.ts)}<br><span style="font-size:10px">${esc(e.source || "")} · ${esc(String(e.bootId || "").slice(0, 8))}</span></div>
|
|
790
|
+
</div>
|
|
791
|
+
`).join("") || `<div class="empty">Aucun événement remonté (scope « Boîte noire » désactivé ?)</div>`}
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
`;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function renderPilotage() {
|
|
798
|
+
const installId = state.server.server.installId;
|
|
799
|
+
const cmds = await api("/ops/api/commands");
|
|
800
|
+
const mine = (cmds.commands || []).filter((c) => c.installId === installId).slice().reverse();
|
|
801
|
+
$("content").innerHTML = `
|
|
802
|
+
<div class="card">
|
|
803
|
+
<h3 style="color:var(--fg);font-size:15px">Pilotage distant</h3>
|
|
804
|
+
<p class="hint" style="margin:8px 0 0">Exécuté seulement si le client a activé « Accepter le pilotage distant ».</p>
|
|
805
|
+
<div class="actions">
|
|
806
|
+
<button class="btn" data-cmd="sync-now">Sync maintenant</button>
|
|
807
|
+
<button class="btn ghost" data-cmd="force-update-check">Vérifier les mises à jour</button>
|
|
808
|
+
<button class="btn ghost" data-cmd="restart-n8n">Redémarrer n8n</button>
|
|
809
|
+
<button class="btn ghost" data-cmd="restart-hermes">Redémarrer Hermes</button>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
<div style="height:16px"></div>
|
|
813
|
+
<div class="card">
|
|
814
|
+
<h3 style="color:var(--fg);font-size:15px;margin-bottom:12px">Historique</h3>
|
|
815
|
+
<div class="list timeline">
|
|
816
|
+
${mine.map((c) => `
|
|
817
|
+
<div class="row">
|
|
818
|
+
<div class="body">
|
|
819
|
+
<div class="title" style="font-size:13px">${esc(c.command)} <span class="pill">${esc(c.status)}</span></div>
|
|
820
|
+
<div class="desc">${esc(c.resultDetail || "")}</div>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="time">${ago(c.createdAt)}</div>
|
|
823
|
+
</div>
|
|
824
|
+
`).join("") || `<div class="empty">Aucune commande</div>`}
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
`;
|
|
828
|
+
$("content").querySelectorAll("[data-cmd]").forEach((btn) => {
|
|
829
|
+
btn.onclick = async () => {
|
|
830
|
+
btn.disabled = true;
|
|
831
|
+
await api("/ops/api/commands", {
|
|
832
|
+
method: "POST",
|
|
833
|
+
body: JSON.stringify({ installId, command: btn.dataset.cmd }),
|
|
834
|
+
});
|
|
835
|
+
await renderPilotage();
|
|
836
|
+
};
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function esc(s) {
|
|
841
|
+
return String(s ?? "")
|
|
842
|
+
.replace(/&/g, "&")
|
|
843
|
+
.replace(/</g, "<")
|
|
844
|
+
.replace(/>/g, ">")
|
|
845
|
+
.replace(/"/g, """);
|
|
846
|
+
}
|
|
847
|
+
function escAttr(s) { return esc(s).replace(/'/g, "'"); }
|
|
848
|
+
|
|
849
|
+
async function render() {
|
|
850
|
+
parseHash();
|
|
851
|
+
const { slug, userId, tab } = state.route;
|
|
852
|
+
|
|
853
|
+
if (!state.fleet) state.fleet = await api("/ops/api/fleet");
|
|
854
|
+
$("sideStats").textContent =
|
|
855
|
+
`${state.fleet.stats.online} en ligne · ${state.fleet.stats.servers} serveur(s)`;
|
|
856
|
+
|
|
857
|
+
if (!slug) {
|
|
858
|
+
state.server = null;
|
|
859
|
+
state.user = null;
|
|
860
|
+
renderSidebar();
|
|
861
|
+
renderHome();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
state.server = await api("/ops/api/server/" + encodeURIComponent(slug));
|
|
866
|
+
if (!state.server.ok) {
|
|
867
|
+
$("content").innerHTML = `<div class="empty">Serveur introuvable</div>`;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (userId) {
|
|
872
|
+
const u = await api(
|
|
873
|
+
`/ops/api/server/${encodeURIComponent(slug)}/user/${encodeURIComponent(userId)}`,
|
|
874
|
+
);
|
|
875
|
+
state.user = u.ok ? u : null;
|
|
876
|
+
} else {
|
|
877
|
+
state.user = null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
renderSidebar();
|
|
881
|
+
|
|
882
|
+
if (userId && state.user) {
|
|
883
|
+
const u = state.user.user;
|
|
884
|
+
$("crumb").innerHTML = `<button type="button" id="cServers">Flotte</button> / <button type="button" id="cServer">${esc(slug)}</button> / ${esc(u.username)}`;
|
|
885
|
+
$("pageTitle").textContent = u.username;
|
|
886
|
+
$("pageSub").textContent = `${u.role} · ${slug} · ${pillOnline(u.online)}`.replace(/<[^>]+>/g, "");
|
|
887
|
+
$("pageSub").innerHTML = `${esc(u.role)} sur <strong>${esc(slug)}</strong> · ${pillOnline(u.online)}`;
|
|
888
|
+
$("cServers").onclick = () => go(null);
|
|
889
|
+
$("cServer").onclick = () => go(slug, null, "overview");
|
|
890
|
+
setTabs([
|
|
891
|
+
{ id: "activity", label: "Activité" },
|
|
892
|
+
{ id: "pages", label: "Temps / pages" },
|
|
893
|
+
{ id: "conversations", label: "Conversations" },
|
|
894
|
+
{ id: "incidents", label: "Incidents" },
|
|
895
|
+
], tab === "overview" ? "activity" : tab);
|
|
896
|
+
const t = tab === "overview" ? "activity" : tab;
|
|
897
|
+
if (t === "conversations") renderConversations(state.user.conversations);
|
|
898
|
+
else if (t === "incidents") renderIncidents();
|
|
899
|
+
else if (t === "pages") renderHeatmap(state.user.heatmap);
|
|
900
|
+
else renderActivity(state.user.activity);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const s = state.server.server;
|
|
905
|
+
$("crumb").innerHTML = `<button type="button" id="cServers">Flotte</button> / ${esc(slug)}`;
|
|
906
|
+
$("cServers").onclick = () => go(null);
|
|
907
|
+
$("pageTitle").textContent = slug;
|
|
908
|
+
$("pageSub").innerHTML = `${esc(s.hostname)} · v${esc(s.appVersion)} · ${pillOnline(s.online)}`;
|
|
909
|
+
setTabs([
|
|
910
|
+
{ id: "overview", label: "Vue d’ensemble" },
|
|
911
|
+
{ id: "users", label: "Utilisateurs" },
|
|
912
|
+
{ id: "activity", label: "Activité" },
|
|
913
|
+
{ id: "conversations", label: "Conversations" },
|
|
914
|
+
{ id: "health", label: "Santé" },
|
|
915
|
+
{ id: "plugins", label: "Plugins" },
|
|
916
|
+
{ id: "consent", label: "Consentement" },
|
|
917
|
+
{ id: "incidents", label: "Incidents" },
|
|
918
|
+
{ id: "ops", label: "Boîte noire" },
|
|
919
|
+
{ id: "pilotage", label: "Pilotage" },
|
|
920
|
+
], tab);
|
|
921
|
+
if (tab === "users") renderUsers();
|
|
922
|
+
else if (tab === "activity") renderActivity(state.server.activity);
|
|
923
|
+
else if (tab === "conversations") renderConversations(state.server.conversations);
|
|
924
|
+
else if (tab === "health") renderHealth();
|
|
925
|
+
else if (tab === "plugins") renderPlugins();
|
|
926
|
+
else if (tab === "consent") renderConsent();
|
|
927
|
+
else if (tab === "incidents") renderIncidents();
|
|
928
|
+
else if (tab === "ops") renderOps();
|
|
929
|
+
else if (tab === "pilotage") await renderPilotage();
|
|
930
|
+
else renderServerOverview();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
window.addEventListener("hashchange", () => { void render(); });
|
|
934
|
+
void render();
|
|
935
|
+
setInterval(() => {
|
|
936
|
+
state.fleet = null;
|
|
937
|
+
void render();
|
|
938
|
+
}, 30000);
|
|
939
|
+
</script>
|
|
940
|
+
</body>
|
|
941
|
+
</html>
|