@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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Résolution env fleet-collector — defaults neutres `CREEZIO_*` / `FLEET_*`
|
|
3
|
+
* + dual-read legacy marques (`TF2_*`, `CERTIVAN_*`).
|
|
4
|
+
*
|
|
5
|
+
* Aucun domaine marque hardcodé : suffix tunnel / titres UI via injection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function firstDefined(...keys) {
|
|
9
|
+
for (const k of keys) {
|
|
10
|
+
const v = process.env[k];
|
|
11
|
+
if (v !== undefined && v !== "") return v;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseJsonEnv(raw, fallback) {
|
|
17
|
+
if (!raw) return fallback;
|
|
18
|
+
try {
|
|
19
|
+
const v = JSON.parse(raw);
|
|
20
|
+
return v && typeof v === "object" ? v : fallback;
|
|
21
|
+
} catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @returns {{
|
|
28
|
+
* port: number,
|
|
29
|
+
* ingestToken: string,
|
|
30
|
+
* opsToken: string,
|
|
31
|
+
* opsUser: string,
|
|
32
|
+
* opsPass: string,
|
|
33
|
+
* dataDir: string | undefined,
|
|
34
|
+
* tunnelSuffix: string,
|
|
35
|
+
* publicDomain: string,
|
|
36
|
+
* ui: {
|
|
37
|
+
* title: string,
|
|
38
|
+
* mark: string,
|
|
39
|
+
* homeTitle: string,
|
|
40
|
+
* realm: string,
|
|
41
|
+
* extrasTitle: string,
|
|
42
|
+
* etatLabels: Record<string, string>,
|
|
43
|
+
* },
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
export function resolveFleetCollectorEnv(defaults = {}) {
|
|
47
|
+
const port = Number(
|
|
48
|
+
firstDefined(
|
|
49
|
+
"CREEZIO_FLEET_PORT",
|
|
50
|
+
"FLEET_PORT",
|
|
51
|
+
"TF2_FLEET_PORT",
|
|
52
|
+
"CERTIVAN_FLEET_PORT",
|
|
53
|
+
) || defaults.port || 8665,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const ingestToken =
|
|
57
|
+
firstDefined(
|
|
58
|
+
"CREEZIO_FLEET_INGEST_TOKEN",
|
|
59
|
+
"FLEET_INGEST_TOKEN",
|
|
60
|
+
"TF2_FLEET_INGEST_TOKEN",
|
|
61
|
+
"CERTIVAN_FLEET_INGEST_TOKEN",
|
|
62
|
+
) || defaults.ingestToken || "";
|
|
63
|
+
|
|
64
|
+
const opsToken =
|
|
65
|
+
firstDefined(
|
|
66
|
+
"CREEZIO_FLEET_OPS_TOKEN",
|
|
67
|
+
"FLEET_OPS_TOKEN",
|
|
68
|
+
"TF2_FLEET_OPS_TOKEN",
|
|
69
|
+
"CERTIVAN_FLEET_OPS_TOKEN",
|
|
70
|
+
) || defaults.opsToken || "";
|
|
71
|
+
|
|
72
|
+
const opsUser =
|
|
73
|
+
firstDefined(
|
|
74
|
+
"CREEZIO_FLEET_OPS_USER",
|
|
75
|
+
"FLEET_OPS_USER",
|
|
76
|
+
"TF2_FLEET_OPS_USER",
|
|
77
|
+
"CERTIVAN_FLEET_OPS_USER",
|
|
78
|
+
) || defaults.opsUser || "ops";
|
|
79
|
+
|
|
80
|
+
const opsPass =
|
|
81
|
+
firstDefined(
|
|
82
|
+
"CREEZIO_FLEET_OPS_PASS",
|
|
83
|
+
"FLEET_OPS_PASS",
|
|
84
|
+
"TF2_FLEET_OPS_PASS",
|
|
85
|
+
"CERTIVAN_FLEET_OPS_PASS",
|
|
86
|
+
) || defaults.opsPass || "ops";
|
|
87
|
+
|
|
88
|
+
const dataDir = firstDefined(
|
|
89
|
+
"CREEZIO_FLEET_DIR",
|
|
90
|
+
"FLEET_DIR",
|
|
91
|
+
"TF2_FLEET_DIR",
|
|
92
|
+
"CERTIVAN_FLEET_DIR",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const publicDomain =
|
|
96
|
+
firstDefined(
|
|
97
|
+
"FLEET_PUBLIC_DOMAIN",
|
|
98
|
+
"CREEZIO_FLEET_DOMAIN",
|
|
99
|
+
"FLEET_DOMAIN",
|
|
100
|
+
) || defaults.publicDomain || "";
|
|
101
|
+
|
|
102
|
+
const tunnelSuffix =
|
|
103
|
+
firstDefined(
|
|
104
|
+
"CREEZIO_FLEET_TUNNEL_SUFFIX",
|
|
105
|
+
"FLEET_TUNNEL_SUFFIX",
|
|
106
|
+
) ||
|
|
107
|
+
defaults.tunnelSuffix ||
|
|
108
|
+
// Si seul le domaine public est fourni, l’utiliser comme suffixe slug.
|
|
109
|
+
publicDomain ||
|
|
110
|
+
"";
|
|
111
|
+
|
|
112
|
+
const title =
|
|
113
|
+
firstDefined(
|
|
114
|
+
"CREEZIO_FLEET_UI_TITLE",
|
|
115
|
+
"FLEET_UI_TITLE",
|
|
116
|
+
) || defaults.uiTitle || "Fleet";
|
|
117
|
+
|
|
118
|
+
const mark =
|
|
119
|
+
firstDefined(
|
|
120
|
+
"CREEZIO_FLEET_UI_MARK",
|
|
121
|
+
"FLEET_UI_MARK",
|
|
122
|
+
) || defaults.uiMark || "FL";
|
|
123
|
+
|
|
124
|
+
const homeTitle =
|
|
125
|
+
firstDefined(
|
|
126
|
+
"CREEZIO_FLEET_UI_HOME_TITLE",
|
|
127
|
+
"FLEET_UI_HOME_TITLE",
|
|
128
|
+
) || defaults.uiHomeTitle || `Flotte ${title}`;
|
|
129
|
+
|
|
130
|
+
const realm =
|
|
131
|
+
firstDefined(
|
|
132
|
+
"CREEZIO_FLEET_UI_REALM",
|
|
133
|
+
"FLEET_UI_REALM",
|
|
134
|
+
) || defaults.uiRealm || title;
|
|
135
|
+
|
|
136
|
+
const extrasTitle =
|
|
137
|
+
firstDefined(
|
|
138
|
+
"CREEZIO_FLEET_UI_EXTRAS_TITLE",
|
|
139
|
+
"FLEET_UI_EXTRAS_TITLE",
|
|
140
|
+
) || defaults.uiExtrasTitle || "Dossiers";
|
|
141
|
+
|
|
142
|
+
const etatLabels = parseJsonEnv(
|
|
143
|
+
firstDefined(
|
|
144
|
+
"CREEZIO_FLEET_UI_ETAT_LABELS",
|
|
145
|
+
"FLEET_UI_ETAT_LABELS",
|
|
146
|
+
),
|
|
147
|
+
defaults.etatLabels || {},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
port,
|
|
152
|
+
ingestToken,
|
|
153
|
+
opsToken,
|
|
154
|
+
opsUser,
|
|
155
|
+
opsPass,
|
|
156
|
+
dataDir,
|
|
157
|
+
tunnelSuffix,
|
|
158
|
+
publicDomain,
|
|
159
|
+
ui: {
|
|
160
|
+
title,
|
|
161
|
+
mark,
|
|
162
|
+
homeTitle,
|
|
163
|
+
realm,
|
|
164
|
+
extrasTitle,
|
|
165
|
+
etatLabels,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function hostnameForSlug(slug, tunnelHostname, tunnelSuffix) {
|
|
171
|
+
if (tunnelHostname) return String(tunnelHostname).slice(0, 120);
|
|
172
|
+
if (tunnelSuffix) return `${slug}.${tunnelSuffix}`;
|
|
173
|
+
return String(slug);
|
|
174
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agrégation flotte pour l’UI ops (slug → users → activité).
|
|
3
|
+
* Suffixe tunnel / hostnames : opts.tunnelSuffix (injection marque).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
function onlineOf(lastSeen) {
|
|
10
|
+
if (!lastSeen) return "offline";
|
|
11
|
+
const s = (Date.now() - new Date(lastSeen).getTime()) / 1000;
|
|
12
|
+
if (s < 180) return "online";
|
|
13
|
+
if (s < 900) return "idle";
|
|
14
|
+
return "offline";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(file, fallback) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listJsonDir(dir, limit = 80) {
|
|
26
|
+
if (!fs.existsSync(dir)) return [];
|
|
27
|
+
return fs
|
|
28
|
+
.readdirSync(dir)
|
|
29
|
+
.filter((f) => f.endsWith(".json"))
|
|
30
|
+
.sort()
|
|
31
|
+
.reverse()
|
|
32
|
+
.slice(0, limit)
|
|
33
|
+
.map((f) => {
|
|
34
|
+
try {
|
|
35
|
+
return { file: f, ...readJson(path.join(dir, f), {}) };
|
|
36
|
+
} catch {
|
|
37
|
+
return { file: f };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function installsForSlug(installsMap, slug) {
|
|
43
|
+
return Object.values(installsMap).filter(
|
|
44
|
+
(i) => String(i.tunnelSlug || "") === slug || (!slug && !i.tunnelSlug),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deriveUsers(install) {
|
|
49
|
+
const sessions = Array.isArray(install.sessions) ? install.sessions : [];
|
|
50
|
+
const sessionByUser = new Map(
|
|
51
|
+
sessions.map((s) => [String(s.userId || ""), s]),
|
|
52
|
+
);
|
|
53
|
+
const users = Array.isArray(install.users) ? install.users : [];
|
|
54
|
+
if (users.length) {
|
|
55
|
+
return users.map((u) => {
|
|
56
|
+
const id = String(u.id || u.userId || u.username || "unknown");
|
|
57
|
+
const sess = sessionByUser.get(id);
|
|
58
|
+
const lastSeen = sess?.lastSeen || install.lastSeen || null;
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
username: String(
|
|
62
|
+
u.username || sess?.username || u.email || u.id || "utilisateur",
|
|
63
|
+
),
|
|
64
|
+
role: String(u.role || "collaborator"),
|
|
65
|
+
kind: String(u.kind || "human"),
|
|
66
|
+
active: u.active !== false,
|
|
67
|
+
online: onlineOf(lastSeen),
|
|
68
|
+
lastSeen,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (sessions.length) {
|
|
73
|
+
return sessions.map((s) => ({
|
|
74
|
+
id: String(s.userId || "unknown"),
|
|
75
|
+
username: String(s.username || s.userId || "utilisateur"),
|
|
76
|
+
role: "collaborator",
|
|
77
|
+
kind: "human",
|
|
78
|
+
active: true,
|
|
79
|
+
online: onlineOf(s.lastSeen),
|
|
80
|
+
lastSeen: s.lastSeen || null,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
// Fallback : compte hôte implicite
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
id: "host",
|
|
87
|
+
username: install.hostname || "hôte",
|
|
88
|
+
role: "owner",
|
|
89
|
+
kind: "human",
|
|
90
|
+
active: true,
|
|
91
|
+
online: onlineOf(install.lastSeen),
|
|
92
|
+
lastSeen: install.lastSeen || null,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function conversationsFromBundles(bundles, installId) {
|
|
98
|
+
const mine = bundles.filter(
|
|
99
|
+
(b) =>
|
|
100
|
+
b.installId === installId &&
|
|
101
|
+
(b.kind === "assistant_chats" || b.kind === "hermes_chats"),
|
|
102
|
+
);
|
|
103
|
+
const threads = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const b of mine) {
|
|
106
|
+
const items = Array.isArray(b.items) ? b.items : [];
|
|
107
|
+
if (b.kind === "assistant_chats") {
|
|
108
|
+
for (const m of items) {
|
|
109
|
+
const title = String(m.title || "Conversation").slice(0, 120);
|
|
110
|
+
const key = `${m.conversationId || title}`;
|
|
111
|
+
if (seen.has(key)) continue;
|
|
112
|
+
seen.add(key);
|
|
113
|
+
const related = items.filter(
|
|
114
|
+
(x) =>
|
|
115
|
+
(m.conversationId && x.conversationId === m.conversationId) ||
|
|
116
|
+
(!m.conversationId && (x.title || "Conversation") === title),
|
|
117
|
+
);
|
|
118
|
+
threads.push({
|
|
119
|
+
id: key,
|
|
120
|
+
source: "assistant",
|
|
121
|
+
title,
|
|
122
|
+
updatedAt: related[0]?.createdAt || b.timestamp,
|
|
123
|
+
preview: String(related[0]?.content || "").slice(0, 160),
|
|
124
|
+
messages: related
|
|
125
|
+
.slice()
|
|
126
|
+
.reverse()
|
|
127
|
+
.slice(0, 40)
|
|
128
|
+
.map((x) => ({
|
|
129
|
+
role: x.role,
|
|
130
|
+
content: String(x.content || ""),
|
|
131
|
+
at: x.createdAt,
|
|
132
|
+
})),
|
|
133
|
+
file: b.file,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} else if (b.kind === "hermes_chats") {
|
|
137
|
+
for (const m of items.slice(0, 20)) {
|
|
138
|
+
const title = String(m.path || "Hermes").slice(0, 120);
|
|
139
|
+
const key = `hermes:${title}`;
|
|
140
|
+
if (seen.has(key)) continue;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
threads.push({
|
|
143
|
+
id: key,
|
|
144
|
+
source: "hermes",
|
|
145
|
+
title,
|
|
146
|
+
updatedAt: m.mtime || b.timestamp,
|
|
147
|
+
preview: String(m.preview || "").slice(0, 160),
|
|
148
|
+
messages: [
|
|
149
|
+
{
|
|
150
|
+
role: "assistant",
|
|
151
|
+
content: String(m.preview || ""),
|
|
152
|
+
at: m.mtime,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
file: b.file,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
threads.sort((a, b) =>
|
|
161
|
+
String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")),
|
|
162
|
+
);
|
|
163
|
+
return threads.slice(0, 40);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function activityFrom(install, bundles, crashes) {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const b of bundles) {
|
|
169
|
+
if (b.installId !== install.installId) continue;
|
|
170
|
+
if (b.kind === "actions" && Array.isArray(b.items)) {
|
|
171
|
+
for (const a of b.items) {
|
|
172
|
+
const meta = a.meta && typeof a.meta === "object" ? a.meta : {};
|
|
173
|
+
out.push({
|
|
174
|
+
at: a.at || b.timestamp,
|
|
175
|
+
type: a.type || a.name || "action",
|
|
176
|
+
name: a.name || a.type || "action",
|
|
177
|
+
category: a.category || meta.category || "ui",
|
|
178
|
+
label: a.label || "Action",
|
|
179
|
+
path: a.path,
|
|
180
|
+
referrerPath: a.referrerPath || meta.referrerPath,
|
|
181
|
+
userId: a.userId,
|
|
182
|
+
username: a.username,
|
|
183
|
+
sessionId: a.sessionId || meta.sessionId,
|
|
184
|
+
durationMs:
|
|
185
|
+
typeof a.durationMs === "number"
|
|
186
|
+
? a.durationMs
|
|
187
|
+
: typeof meta.durationMs === "number"
|
|
188
|
+
? meta.durationMs
|
|
189
|
+
: undefined,
|
|
190
|
+
meta,
|
|
191
|
+
surface: a.surface || meta.surface || "crm",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (b.kind === "request_logs" && Array.isArray(b.items)) {
|
|
196
|
+
for (const a of b.items.slice(0, 40)) {
|
|
197
|
+
out.push({
|
|
198
|
+
at: a.at || b.timestamp,
|
|
199
|
+
type: "api",
|
|
200
|
+
name: "api.request",
|
|
201
|
+
category: "system",
|
|
202
|
+
label: `${a.method || "HTTP"} ${a.path || ""}`.trim(),
|
|
203
|
+
path: a.path,
|
|
204
|
+
durationMs: a.durationMs,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (b.kind === "assistant_chats" && Array.isArray(b.items)) {
|
|
209
|
+
for (const m of b.items.filter((x) => x.role === "user").slice(0, 20)) {
|
|
210
|
+
out.push({
|
|
211
|
+
at: m.createdAt || b.timestamp,
|
|
212
|
+
type: "chat",
|
|
213
|
+
name: "chat.message",
|
|
214
|
+
category: "chat",
|
|
215
|
+
label: String(m.content || m.title || "Message").slice(0, 140),
|
|
216
|
+
path: "/assistant",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const c of crashes) {
|
|
222
|
+
if (c.installId !== install.installId) continue;
|
|
223
|
+
out.push({
|
|
224
|
+
at: c.timestamp,
|
|
225
|
+
type: "incident",
|
|
226
|
+
name: "system.crash",
|
|
227
|
+
category: "system",
|
|
228
|
+
label: `Incident ${c.kind || "crash"} — ${String(c?.detail?.message || c.message || "").slice(0, 100)}`,
|
|
229
|
+
path: null,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
out.sort((a, b) => String(b.at || "").localeCompare(String(a.at || "")));
|
|
233
|
+
return out.slice(0, 300);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildFleetOverview(dataDir, opts = {}) {
|
|
237
|
+
const tunnelSuffix = opts.tunnelSuffix || "";
|
|
238
|
+
const installsMap = readJson(path.join(dataDir, "installs.json"), {});
|
|
239
|
+
const bySlug = new Map();
|
|
240
|
+
for (const inst of Object.values(installsMap)) {
|
|
241
|
+
const slug = String(inst.tunnelSlug || "(sans-slug)");
|
|
242
|
+
if (!bySlug.has(slug)) bySlug.set(slug, []);
|
|
243
|
+
bySlug.get(slug).push(inst);
|
|
244
|
+
}
|
|
245
|
+
const servers = [];
|
|
246
|
+
for (const [slug, list] of bySlug) {
|
|
247
|
+
list.sort((a, b) =>
|
|
248
|
+
String(b.lastSeen || "").localeCompare(String(a.lastSeen || "")),
|
|
249
|
+
);
|
|
250
|
+
const primary = list[0];
|
|
251
|
+
const users = deriveUsers(primary);
|
|
252
|
+
servers.push({
|
|
253
|
+
slug,
|
|
254
|
+
hostname:
|
|
255
|
+
primary.tunnelHostname ||
|
|
256
|
+
(tunnelSuffix ? `${slug}.${tunnelSuffix}` : slug),
|
|
257
|
+
online: onlineOf(primary.lastSeen),
|
|
258
|
+
lastSeen: primary.lastSeen || null,
|
|
259
|
+
appVersion: primary.appVersion || "?",
|
|
260
|
+
platform: primary.platform || "?",
|
|
261
|
+
health: primary.health || {},
|
|
262
|
+
crashCount: list.reduce((n, i) => n + (i.crashCount || 0), 0),
|
|
263
|
+
userCount: users.length,
|
|
264
|
+
usersOnline: users.filter((u) => u.online === "online").length,
|
|
265
|
+
installId: primary.installId,
|
|
266
|
+
consent: primary.consent || null,
|
|
267
|
+
// Extras marque (opaque) — agrégés si présents (ex. CV dossiers).
|
|
268
|
+
dossierStats: primary.dossierStats || null,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
servers.sort((a, b) => {
|
|
272
|
+
const rank = { online: 0, idle: 1, offline: 2 };
|
|
273
|
+
return (rank[a.online] ?? 9) - (rank[b.online] ?? 9) ||
|
|
274
|
+
String(a.slug).localeCompare(String(b.slug));
|
|
275
|
+
});
|
|
276
|
+
// Agrégats génériques : extras dossierStats + répartition versions.
|
|
277
|
+
const dossiers = { total: 0, parEtat: {}, actifs7j: 0 };
|
|
278
|
+
const versions = {};
|
|
279
|
+
for (const s of servers) {
|
|
280
|
+
const v = s.appVersion || "?";
|
|
281
|
+
versions[v] = (versions[v] || 0) + 1;
|
|
282
|
+
const d = s.dossierStats;
|
|
283
|
+
if (d && typeof d === "object") {
|
|
284
|
+
dossiers.total += Number(d.total) || 0;
|
|
285
|
+
dossiers.actifs7j += Number(d.actifs7j) || 0;
|
|
286
|
+
for (const [etat, n] of Object.entries(d.parEtat || {})) {
|
|
287
|
+
dossiers.parEtat[etat] = (dossiers.parEtat[etat] || 0) + (Number(n) || 0);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
servers,
|
|
294
|
+
stats: {
|
|
295
|
+
servers: servers.length,
|
|
296
|
+
online: servers.filter((s) => s.online === "online").length,
|
|
297
|
+
users: servers.reduce((n, s) => n + s.userCount, 0),
|
|
298
|
+
crashes: servers.reduce((n, s) => n + s.crashCount, 0),
|
|
299
|
+
dossiers,
|
|
300
|
+
versions,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Boîte noire : événements ops (bundles kind=ops_events) d'une install. */
|
|
306
|
+
function opsEventsFromBundles(bundles, installId, limit = 200) {
|
|
307
|
+
const events = [];
|
|
308
|
+
for (const b of bundles) {
|
|
309
|
+
if (b.installId !== installId || b.kind !== "ops_events") continue;
|
|
310
|
+
for (const e of Array.isArray(b.items) ? b.items : []) {
|
|
311
|
+
events.push({
|
|
312
|
+
ts: e.ts,
|
|
313
|
+
bootId: e.bootId,
|
|
314
|
+
seq: e.seq,
|
|
315
|
+
source: e.source,
|
|
316
|
+
level: e.level,
|
|
317
|
+
kind: e.kind,
|
|
318
|
+
outcome: e.outcome,
|
|
319
|
+
reason: e.reason,
|
|
320
|
+
durationMs: e.durationMs,
|
|
321
|
+
ctx: e.ctx,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
events.sort(
|
|
326
|
+
(a, b) =>
|
|
327
|
+
String(b.ts || "").localeCompare(String(a.ts || "")) ||
|
|
328
|
+
(b.seq || 0) - (a.seq || 0),
|
|
329
|
+
);
|
|
330
|
+
return events.slice(0, limit);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function buildServerDetail(dataDir, slug, opts = {}) {
|
|
334
|
+
const tunnelSuffix = opts.tunnelSuffix || "";
|
|
335
|
+
const installsMap = readJson(path.join(dataDir, "installs.json"), {});
|
|
336
|
+
const bundles = listJsonDir(path.join(dataDir, "bundles"), 120);
|
|
337
|
+
const crashes = listJsonDir(path.join(dataDir, "crashes"), 80);
|
|
338
|
+
const list = installsForSlug(installsMap, slug);
|
|
339
|
+
if (!list.length && slug !== "(sans-slug)") {
|
|
340
|
+
// try exact match without filter quirk
|
|
341
|
+
const all = Object.values(installsMap).filter(
|
|
342
|
+
(i) => String(i.tunnelSlug || "(sans-slug)") === slug,
|
|
343
|
+
);
|
|
344
|
+
list.push(...all);
|
|
345
|
+
}
|
|
346
|
+
if (!list.length) return { ok: false, error: "not_found" };
|
|
347
|
+
list.sort((a, b) =>
|
|
348
|
+
String(b.lastSeen || "").localeCompare(String(a.lastSeen || "")),
|
|
349
|
+
);
|
|
350
|
+
const primary = list[0];
|
|
351
|
+
const users = deriveUsers(primary);
|
|
352
|
+
const conversations = conversationsFromBundles(bundles, primary.installId);
|
|
353
|
+
const activity = activityFrom(primary, bundles, crashes);
|
|
354
|
+
const opsEvents = opsEventsFromBundles(bundles, primary.installId);
|
|
355
|
+
const incidents = crashes
|
|
356
|
+
.filter((c) => c.installId === primary.installId)
|
|
357
|
+
.map((c) => ({
|
|
358
|
+
file: c.file,
|
|
359
|
+
at: c.timestamp,
|
|
360
|
+
kind: c.kind,
|
|
361
|
+
message: String(c?.detail?.message || c.message || "").slice(0, 240),
|
|
362
|
+
bootStage: c.bootStage,
|
|
363
|
+
appVersion: c.appVersion,
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
ok: true,
|
|
368
|
+
server: {
|
|
369
|
+
slug,
|
|
370
|
+
hostname:
|
|
371
|
+
primary.tunnelHostname ||
|
|
372
|
+
(tunnelSuffix ? `${slug}.${tunnelSuffix}` : slug),
|
|
373
|
+
online: onlineOf(primary.lastSeen),
|
|
374
|
+
lastSeen: primary.lastSeen,
|
|
375
|
+
appVersion: primary.appVersion,
|
|
376
|
+
platform: primary.platform,
|
|
377
|
+
arch: primary.arch,
|
|
378
|
+
osRelease: primary.osRelease,
|
|
379
|
+
machine: primary.hostname,
|
|
380
|
+
health: primary.health || {},
|
|
381
|
+
hermesStats: primary.hermesStats || null,
|
|
382
|
+
consent: primary.consent || null,
|
|
383
|
+
plugins: Array.isArray(primary.plugins) ? primary.plugins : [],
|
|
384
|
+
sessions: Array.isArray(primary.sessions) ? primary.sessions : [],
|
|
385
|
+
installId: primary.installId,
|
|
386
|
+
crashCount: primary.crashCount || 0,
|
|
387
|
+
dossierStats: primary.dossierStats || null,
|
|
388
|
+
},
|
|
389
|
+
users,
|
|
390
|
+
activity,
|
|
391
|
+
conversations,
|
|
392
|
+
incidents,
|
|
393
|
+
// Boîte noire : dernier résumé de boot (heartbeat) + événements détaillés.
|
|
394
|
+
ops: {
|
|
395
|
+
lastBootSummary: primary.lastBootSummary || null,
|
|
396
|
+
events: opsEvents,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function buildUserDetail(dataDir, slug, userId, opts = {}) {
|
|
402
|
+
const detail = buildServerDetail(dataDir, slug, opts);
|
|
403
|
+
if (!detail.ok) return detail;
|
|
404
|
+
const user =
|
|
405
|
+
detail.users.find((u) => u.id === userId) ||
|
|
406
|
+
detail.users.find((u) => u.username === userId) ||
|
|
407
|
+
null;
|
|
408
|
+
if (!user) return { ok: false, error: "user_not_found" };
|
|
409
|
+
// Events de cet user + orphelins (sans userId) si user unique / owner seul.
|
|
410
|
+
const humans = detail.users.filter((u) => u.kind !== "ai");
|
|
411
|
+
const soleHuman = humans.length === 1 ? humans[0] : null;
|
|
412
|
+
const soleOwner =
|
|
413
|
+
user.role === "owner" &&
|
|
414
|
+
humans.filter((u) => u.role === "owner").length === 1
|
|
415
|
+
? user
|
|
416
|
+
: null;
|
|
417
|
+
const activity = detail.activity.filter((a) => {
|
|
418
|
+
if (a.userId && a.userId === user.id) return true;
|
|
419
|
+
if (a.username && a.username === user.username) return true;
|
|
420
|
+
if (!a.userId && !a.username) {
|
|
421
|
+
if (user.id === "host") return true;
|
|
422
|
+
if (soleHuman && soleHuman.id === user.id) return true;
|
|
423
|
+
if (soleOwner && soleOwner.id === user.id) return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
});
|
|
427
|
+
const pageStats = {};
|
|
428
|
+
for (const a of activity) {
|
|
429
|
+
if (a.name === "page.hide" && a.path && typeof a.durationMs === "number") {
|
|
430
|
+
if (!pageStats[a.path]) pageStats[a.path] = { path: a.path, views: 0, dwellMs: 0 };
|
|
431
|
+
pageStats[a.path].views += 1;
|
|
432
|
+
pageStats[a.path].dwellMs += a.durationMs;
|
|
433
|
+
}
|
|
434
|
+
if (a.name === "page.view" && a.path) {
|
|
435
|
+
if (!pageStats[a.path]) pageStats[a.path] = { path: a.path, views: 0, dwellMs: 0 };
|
|
436
|
+
pageStats[a.path].views += 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const heatmap = Object.values(pageStats)
|
|
440
|
+
.sort((a, b) => b.dwellMs - a.dwellMs || b.views - a.views)
|
|
441
|
+
.slice(0, 30);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
server: detail.server,
|
|
446
|
+
user,
|
|
447
|
+
activity,
|
|
448
|
+
conversations: detail.conversations,
|
|
449
|
+
incidents: detail.incidents,
|
|
450
|
+
heatmap,
|
|
451
|
+
sessions: Array.isArray(detail.server?.sessions)
|
|
452
|
+
? detail.server.sessions
|
|
453
|
+
: [],
|
|
454
|
+
};
|
|
455
|
+
}
|