@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,564 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @creezio/observability fleet-collector — cockpit flotte ops (SoT kit, D-P25).
|
|
4
|
+
*
|
|
5
|
+
* Loopback HTTP + store JSON. Auth :
|
|
6
|
+
* - Ingest clients : path /i-<INGEST_TOKEN>/… (pas de login)
|
|
7
|
+
* - Ops UI/API : HTTP Basic (user/password) — prompt navigateur
|
|
8
|
+
* (Bearer OPS_TOKEN encore accepté pour scripts)
|
|
9
|
+
*
|
|
10
|
+
* Env (neutre + dual-read legacy TF2_* / CERTIVAN_*) — voir env.mjs :
|
|
11
|
+
* CREEZIO_FLEET_PORT | FLEET_PORT | TF2_FLEET_PORT | CERTIVAN_FLEET_PORT
|
|
12
|
+
* CREEZIO_FLEET_INGEST_TOKEN | FLEET_INGEST_TOKEN | …
|
|
13
|
+
* CREEZIO_FLEET_OPS_USER / _PASS / _TOKEN | FLEET_* | TF2_* | CERTIVAN_*
|
|
14
|
+
* CREEZIO_FLEET_DIR | FLEET_DIR | …
|
|
15
|
+
* FLEET_PUBLIC_DOMAIN | CREEZIO_FLEET_DOMAIN
|
|
16
|
+
* CREEZIO_FLEET_TUNNEL_SUFFIX | FLEET_TUNNEL_SUFFIX
|
|
17
|
+
* CREEZIO_FLEET_UI_TITLE | FLEET_UI_TITLE (+ MARK / HOME_TITLE / REALM / EXTRAS_TITLE)
|
|
18
|
+
*
|
|
19
|
+
* 0 domaine marque hardcodé — injection env uniquement.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import http from "node:http";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import crypto from "node:crypto";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import {
|
|
28
|
+
buildFleetOverview,
|
|
29
|
+
buildServerDetail,
|
|
30
|
+
buildUserDetail,
|
|
31
|
+
} from "./ops-api.mjs";
|
|
32
|
+
import { resolveFleetCollectorEnv } from "./env.mjs";
|
|
33
|
+
|
|
34
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const PUBLIC_DIR = path.join(__dirname, "public");
|
|
36
|
+
|
|
37
|
+
const cfg = resolveFleetCollectorEnv();
|
|
38
|
+
const PORT = cfg.port;
|
|
39
|
+
const INGEST = cfg.ingestToken;
|
|
40
|
+
const OPS_TOKEN = cfg.opsToken;
|
|
41
|
+
const OPS_USER = cfg.opsUser;
|
|
42
|
+
const OPS_PASS = cfg.opsPass;
|
|
43
|
+
const TUNNEL_SUFFIX = cfg.tunnelSuffix;
|
|
44
|
+
const UI = cfg.ui;
|
|
45
|
+
const DATA_DIR =
|
|
46
|
+
cfg.dataDir || path.join(process.cwd(), "fleet-data");
|
|
47
|
+
const MAX_BODY = 4 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
if (!INGEST) {
|
|
50
|
+
console.error(
|
|
51
|
+
"[fleet] CREEZIO_FLEET_INGEST_TOKEN (ou FLEET_/TF2_/CERTIVAN_*) requis",
|
|
52
|
+
);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeEqualStr(a, b) {
|
|
57
|
+
const aa = Buffer.from(String(a));
|
|
58
|
+
const bb = Buffer.from(String(b));
|
|
59
|
+
if (aa.length !== bb.length) {
|
|
60
|
+
crypto.timingSafeEqual(aa, aa);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return crypto.timingSafeEqual(aa, bb);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ALLOWED_REMOTE = new Set([
|
|
67
|
+
"force-update-check",
|
|
68
|
+
"restart-n8n",
|
|
69
|
+
"restart-hermes",
|
|
70
|
+
"sync-now",
|
|
71
|
+
"upload-diagnostics",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
75
|
+
fs.mkdirSync(path.join(DATA_DIR, "crashes"), { recursive: true });
|
|
76
|
+
fs.mkdirSync(path.join(DATA_DIR, "bundles"), { recursive: true });
|
|
77
|
+
|
|
78
|
+
const INSTALLS_FILE = path.join(DATA_DIR, "installs.json");
|
|
79
|
+
const COMMANDS_FILE = path.join(DATA_DIR, "commands.json");
|
|
80
|
+
const AUDIT_FILE = path.join(DATA_DIR, "audit.log");
|
|
81
|
+
|
|
82
|
+
function readJson(file, fallback) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
85
|
+
} catch {
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeJson(file, data) {
|
|
91
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
92
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
93
|
+
fs.renameSync(tmp, file);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sanitize(s, fallback = "x") {
|
|
97
|
+
const v = String(s || "").replace(/[^A-Za-z0-9_.-]/g, "").slice(0, 64);
|
|
98
|
+
return v || fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function audit(line) {
|
|
102
|
+
const row = `${new Date().toISOString()} ${line}\n`;
|
|
103
|
+
try {
|
|
104
|
+
fs.appendFileSync(AUDIT_FILE, row);
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
console.log(`[fleet] ${line}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadInstalls() {
|
|
112
|
+
const raw = readJson(INSTALLS_FILE, {});
|
|
113
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function saveInstalls(map) {
|
|
117
|
+
writeJson(INSTALLS_FILE, map);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadCommands() {
|
|
121
|
+
const raw = readJson(COMMANDS_FILE, []);
|
|
122
|
+
return Array.isArray(raw) ? raw : [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function saveCommands(list) {
|
|
126
|
+
writeJson(COMMANDS_FILE, list);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function upsertInstall(installId, patch) {
|
|
130
|
+
const id = sanitize(installId, "anon");
|
|
131
|
+
const map = loadInstalls();
|
|
132
|
+
const prev = map[id] || { installId: id, firstSeen: new Date().toISOString() };
|
|
133
|
+
map[id] = {
|
|
134
|
+
...prev,
|
|
135
|
+
...patch,
|
|
136
|
+
installId: id,
|
|
137
|
+
lastSeen: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
saveInstalls(map);
|
|
140
|
+
return map[id];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readBody(req) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let size = 0;
|
|
146
|
+
const chunks = [];
|
|
147
|
+
req.on("data", (c) => {
|
|
148
|
+
size += c.length;
|
|
149
|
+
if (size > MAX_BODY) {
|
|
150
|
+
reject(new Error("too large"));
|
|
151
|
+
req.destroy();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
chunks.push(c);
|
|
155
|
+
});
|
|
156
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
157
|
+
req.on("error", reject);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function send(res, code, body, headers = {}) {
|
|
162
|
+
const payload =
|
|
163
|
+
body === undefined || body === null
|
|
164
|
+
? ""
|
|
165
|
+
: typeof body === "string"
|
|
166
|
+
? body
|
|
167
|
+
: JSON.stringify(body);
|
|
168
|
+
const h = {
|
|
169
|
+
"Content-Type":
|
|
170
|
+
typeof body === "string" && body.trimStart().startsWith("<")
|
|
171
|
+
? "text/html; charset=utf-8"
|
|
172
|
+
: "application/json; charset=utf-8",
|
|
173
|
+
...headers,
|
|
174
|
+
};
|
|
175
|
+
if (!payload) delete h["Content-Type"];
|
|
176
|
+
res.writeHead(code, h);
|
|
177
|
+
res.end(payload);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseBasicAuth(header) {
|
|
181
|
+
if (!header || !header.startsWith("Basic ")) return null;
|
|
182
|
+
try {
|
|
183
|
+
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
|
|
184
|
+
const i = decoded.indexOf(":");
|
|
185
|
+
if (i < 0) return null;
|
|
186
|
+
return { user: decoded.slice(0, i), pass: decoded.slice(i + 1) };
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function opsAuthorized(req, url) {
|
|
193
|
+
const auth = req.headers.authorization || "";
|
|
194
|
+
if (OPS_TOKEN && auth === `Bearer ${OPS_TOKEN}`) return true;
|
|
195
|
+
if (OPS_TOKEN && url.searchParams.get("token") === OPS_TOKEN) return true;
|
|
196
|
+
const basic = parseBasicAuth(auth);
|
|
197
|
+
if (
|
|
198
|
+
basic &&
|
|
199
|
+
safeEqualStr(basic.user, OPS_USER) &&
|
|
200
|
+
safeEqualStr(basic.pass, OPS_PASS)
|
|
201
|
+
) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function sendUnauthorized(res) {
|
|
208
|
+
send(
|
|
209
|
+
res,
|
|
210
|
+
401,
|
|
211
|
+
{ ok: false, error: "unauthorized" },
|
|
212
|
+
{
|
|
213
|
+
"WWW-Authenticate": `Basic realm="${UI.realm.replace(/"/g, "")}", charset="UTF-8"`,
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseIngestPath(pathname) {
|
|
219
|
+
const prefix = `/i-${INGEST}`;
|
|
220
|
+
if (!pathname.startsWith(prefix)) return null;
|
|
221
|
+
return pathname.slice(prefix.length) || "/";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function renderOpsHtml() {
|
|
225
|
+
const raw = fs.readFileSync(path.join(PUBLIC_DIR, "index.html"), "utf8");
|
|
226
|
+
const brand = {
|
|
227
|
+
title: UI.title,
|
|
228
|
+
mark: UI.mark,
|
|
229
|
+
homeTitle: UI.homeTitle,
|
|
230
|
+
extrasTitle: UI.extrasTitle,
|
|
231
|
+
etatLabels: UI.etatLabels,
|
|
232
|
+
};
|
|
233
|
+
const inject = `<script>window.__FLEET_BRAND__=${JSON.stringify(brand)};</script>`;
|
|
234
|
+
let html = raw;
|
|
235
|
+
if (html.includes("</head>")) {
|
|
236
|
+
html = html.replace("</head>", `${inject}</head>`);
|
|
237
|
+
} else {
|
|
238
|
+
html = inject + html;
|
|
239
|
+
}
|
|
240
|
+
html = html
|
|
241
|
+
.replace(/__FLEET_UI_TITLE__/g, UI.title)
|
|
242
|
+
.replace(/__FLEET_UI_MARK__/g, UI.mark);
|
|
243
|
+
return html;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function handleIngest(req, res, subPath, url) {
|
|
247
|
+
if (req.method === "POST" && subPath === "/heartbeat") {
|
|
248
|
+
let body;
|
|
249
|
+
try {
|
|
250
|
+
body = JSON.parse(await readBody(req));
|
|
251
|
+
} catch {
|
|
252
|
+
return send(res, 400, { ok: false, error: "json" });
|
|
253
|
+
}
|
|
254
|
+
const installId = sanitize(body.installId, "anon");
|
|
255
|
+
upsertInstall(installId, {
|
|
256
|
+
appVersion: sanitize(body.appVersion, "?"),
|
|
257
|
+
platform: sanitize(body.platform),
|
|
258
|
+
arch: sanitize(body.arch),
|
|
259
|
+
osRelease: String(body.osRelease || "").slice(0, 40),
|
|
260
|
+
hostname: String(body.hostname || "").slice(0, 80),
|
|
261
|
+
tunnelSlug: body.tunnelSlug ? sanitize(body.tunnelSlug) : null,
|
|
262
|
+
tunnelHostname: body.tunnelHostname
|
|
263
|
+
? String(body.tunnelHostname).slice(0, 120)
|
|
264
|
+
: null,
|
|
265
|
+
consent: body.consent || null,
|
|
266
|
+
health: body.health || null,
|
|
267
|
+
plugins: Array.isArray(body.plugins) ? body.plugins.slice(0, 50) : undefined,
|
|
268
|
+
users: Array.isArray(body.users) ? body.users.slice(0, 100) : undefined,
|
|
269
|
+
sessions: Array.isArray(body.sessions)
|
|
270
|
+
? body.sessions.slice(0, 100)
|
|
271
|
+
: undefined,
|
|
272
|
+
hermesStats: body.hermesStats || undefined,
|
|
273
|
+
// Extras marque (ex. dossierStats CV via getHeartbeatExtras) — opaque.
|
|
274
|
+
dossierStats:
|
|
275
|
+
body.dossierStats && typeof body.dossierStats === "object"
|
|
276
|
+
? body.dossierStats
|
|
277
|
+
: undefined,
|
|
278
|
+
// Boîte noire : résumé du dernier boot (décisions + durées + compteurs).
|
|
279
|
+
lastBootSummary:
|
|
280
|
+
body.lastBootSummary && typeof body.lastBootSummary === "object"
|
|
281
|
+
? body.lastBootSummary
|
|
282
|
+
: undefined,
|
|
283
|
+
lastHeartbeat: new Date().toISOString(),
|
|
284
|
+
});
|
|
285
|
+
audit(`heartbeat install=${installId} v=${sanitize(body.appVersion)}`);
|
|
286
|
+
return send(res, 200, { ok: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (req.method === "POST" && subPath === "/crash") {
|
|
290
|
+
let body;
|
|
291
|
+
try {
|
|
292
|
+
body = JSON.parse(await readBody(req));
|
|
293
|
+
} catch {
|
|
294
|
+
return send(res, 400, { ok: false });
|
|
295
|
+
}
|
|
296
|
+
const installId = sanitize(body.installId, "anon");
|
|
297
|
+
const kind = sanitize(body.kind, "unknown");
|
|
298
|
+
const ts = new Date().toISOString();
|
|
299
|
+
const name = `${ts.replace(/[:.]/g, "-")}_${kind}_${installId.slice(0, 8)}.json`;
|
|
300
|
+
fs.writeFileSync(
|
|
301
|
+
path.join(DATA_DIR, "crashes", name),
|
|
302
|
+
JSON.stringify(body, null, 2),
|
|
303
|
+
);
|
|
304
|
+
const map = loadInstalls();
|
|
305
|
+
const prev = map[installId] || { installId, firstSeen: ts };
|
|
306
|
+
prev.crashCount = (prev.crashCount || 0) + 1;
|
|
307
|
+
prev.lastCrashAt = ts;
|
|
308
|
+
prev.lastCrashKind = kind;
|
|
309
|
+
prev.lastSeen = ts;
|
|
310
|
+
map[installId] = prev;
|
|
311
|
+
saveInstalls(map);
|
|
312
|
+
audit(`crash install=${installId} kind=${kind}`);
|
|
313
|
+
return send(res, 204);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (req.method === "POST" && subPath === "/bundle") {
|
|
317
|
+
let body;
|
|
318
|
+
try {
|
|
319
|
+
body = JSON.parse(await readBody(req));
|
|
320
|
+
} catch {
|
|
321
|
+
return send(res, 400, { ok: false });
|
|
322
|
+
}
|
|
323
|
+
const installId = sanitize(body.installId, "anon");
|
|
324
|
+
const kind = sanitize(body.kind, "bundle");
|
|
325
|
+
const ts = new Date().toISOString();
|
|
326
|
+
const name = `${ts.replace(/[:.]/g, "-")}_${kind}_${installId.slice(0, 8)}.json`;
|
|
327
|
+
fs.writeFileSync(
|
|
328
|
+
path.join(DATA_DIR, "bundles", name),
|
|
329
|
+
JSON.stringify(body, null, 2),
|
|
330
|
+
);
|
|
331
|
+
upsertInstall(installId, {
|
|
332
|
+
[`lastBundle_${kind}`]: ts,
|
|
333
|
+
});
|
|
334
|
+
audit(`bundle install=${installId} kind=${kind}`);
|
|
335
|
+
return send(res, 200, { ok: true });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (req.method === "GET" && subPath === "/commands") {
|
|
339
|
+
const installId = sanitize(url.searchParams.get("installId"), "");
|
|
340
|
+
if (!installId) return send(res, 400, { ok: false });
|
|
341
|
+
const pending = loadCommands().filter(
|
|
342
|
+
(c) =>
|
|
343
|
+
c.installId === installId &&
|
|
344
|
+
c.status === "pending" &&
|
|
345
|
+
ALLOWED_REMOTE.has(c.command),
|
|
346
|
+
);
|
|
347
|
+
return send(res, 200, { ok: true, commands: pending.slice(0, 10) });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (req.method === "POST" && subPath === "/commands/ack") {
|
|
351
|
+
let body;
|
|
352
|
+
try {
|
|
353
|
+
body = JSON.parse(await readBody(req));
|
|
354
|
+
} catch {
|
|
355
|
+
return send(res, 400, { ok: false });
|
|
356
|
+
}
|
|
357
|
+
const list = loadCommands();
|
|
358
|
+
const idx = list.findIndex((c) => c.id === body.commandId);
|
|
359
|
+
if (idx >= 0) {
|
|
360
|
+
list[idx] = {
|
|
361
|
+
...list[idx],
|
|
362
|
+
status: body.ok ? "done" : "failed",
|
|
363
|
+
resultDetail: String(body.detail || "").slice(0, 500),
|
|
364
|
+
ackedAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
saveCommands(list);
|
|
367
|
+
audit(
|
|
368
|
+
`cmd-ack id=${body.commandId} ok=${body.ok} install=${sanitize(body.installId)}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return send(res, 200, { ok: true });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return send(res, 404, { ok: false });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function listCrashes(limit = 50) {
|
|
378
|
+
const dir = path.join(DATA_DIR, "crashes");
|
|
379
|
+
return fs
|
|
380
|
+
.readdirSync(dir)
|
|
381
|
+
.filter((f) => f.endsWith(".json"))
|
|
382
|
+
.sort()
|
|
383
|
+
.reverse()
|
|
384
|
+
.slice(0, limit)
|
|
385
|
+
.map((f) => {
|
|
386
|
+
try {
|
|
387
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
388
|
+
return {
|
|
389
|
+
file: f,
|
|
390
|
+
kind: j.kind,
|
|
391
|
+
installId: j.installId,
|
|
392
|
+
appVersion: j.appVersion,
|
|
393
|
+
timestamp: j.timestamp,
|
|
394
|
+
bootStage: j.bootStage,
|
|
395
|
+
message: String(j?.detail?.message || "").slice(0, 200),
|
|
396
|
+
};
|
|
397
|
+
} catch {
|
|
398
|
+
return { file: f };
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function listBundles(limit = 40) {
|
|
404
|
+
const dir = path.join(DATA_DIR, "bundles");
|
|
405
|
+
return fs
|
|
406
|
+
.readdirSync(dir)
|
|
407
|
+
.filter((f) => f.endsWith(".json"))
|
|
408
|
+
.sort()
|
|
409
|
+
.reverse()
|
|
410
|
+
.slice(0, limit)
|
|
411
|
+
.map((f) => {
|
|
412
|
+
try {
|
|
413
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
414
|
+
return {
|
|
415
|
+
file: f,
|
|
416
|
+
kind: j.kind,
|
|
417
|
+
installId: j.installId,
|
|
418
|
+
timestamp: j.timestamp,
|
|
419
|
+
count: Array.isArray(j.items) ? j.items.length : 0,
|
|
420
|
+
};
|
|
421
|
+
} catch {
|
|
422
|
+
return { file: f };
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const opsOpts = () => ({ tunnelSuffix: TUNNEL_SUFFIX });
|
|
428
|
+
|
|
429
|
+
async function handleOps(req, res, url) {
|
|
430
|
+
const p = url.pathname;
|
|
431
|
+
|
|
432
|
+
if (!opsAuthorized(req, url)) {
|
|
433
|
+
return sendUnauthorized(res);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (req.method === "GET" && (p === "/" || p === "/ops" || p === "/ops/")) {
|
|
437
|
+
return send(res, 200, renderOpsHtml());
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (req.method === "GET" && p === "/ops/api/fleet") {
|
|
441
|
+
return send(res, 200, buildFleetOverview(DATA_DIR, opsOpts()));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (req.method === "GET" && p.startsWith("/ops/api/server/") && p.includes("/user/")) {
|
|
445
|
+
const rest = p.slice("/ops/api/server/".length);
|
|
446
|
+
const [slugEnc, , userEnc] = rest.split("/");
|
|
447
|
+
const detail = buildUserDetail(
|
|
448
|
+
DATA_DIR,
|
|
449
|
+
decodeURIComponent(slugEnc || ""),
|
|
450
|
+
decodeURIComponent(userEnc || ""),
|
|
451
|
+
opsOpts(),
|
|
452
|
+
);
|
|
453
|
+
return send(res, detail.ok ? 200 : 404, detail);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (req.method === "GET" && p.startsWith("/ops/api/server/")) {
|
|
457
|
+
const slug = decodeURIComponent(p.slice("/ops/api/server/".length));
|
|
458
|
+
const detail = buildServerDetail(DATA_DIR, slug, opsOpts());
|
|
459
|
+
return send(res, detail.ok ? 200 : 404, detail);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (req.method === "GET" && p === "/ops/api/installs") {
|
|
463
|
+
const map = loadInstalls();
|
|
464
|
+
const list = Object.values(map).sort((a, b) =>
|
|
465
|
+
String(b.lastSeen || "").localeCompare(String(a.lastSeen || "")),
|
|
466
|
+
);
|
|
467
|
+
return send(res, 200, { ok: true, installs: list });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (req.method === "GET" && p === "/ops/api/crashes") {
|
|
471
|
+
return send(res, 200, { ok: true, crashes: listCrashes(80) });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (req.method === "GET" && p === "/ops/api/bundles") {
|
|
475
|
+
return send(res, 200, { ok: true, bundles: listBundles(60) });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (req.method === "GET" && p.startsWith("/ops/api/bundle/")) {
|
|
479
|
+
const file = sanitize(p.slice("/ops/api/bundle/".length), "");
|
|
480
|
+
const full = path.join(DATA_DIR, "bundles", file);
|
|
481
|
+
if (!file.endsWith(".json") || !fs.existsSync(full)) {
|
|
482
|
+
return send(res, 404, { ok: false });
|
|
483
|
+
}
|
|
484
|
+
return send(res, 200, readJson(full, {}));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (req.method === "GET" && p.startsWith("/ops/api/crash/")) {
|
|
488
|
+
const file = sanitize(p.slice("/ops/api/crash/".length), "");
|
|
489
|
+
const full = path.join(DATA_DIR, "crashes", file);
|
|
490
|
+
if (!file.endsWith(".json") || !fs.existsSync(full)) {
|
|
491
|
+
return send(res, 404, { ok: false });
|
|
492
|
+
}
|
|
493
|
+
return send(res, 200, readJson(full, {}));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (req.method === "GET" && p === "/ops/api/commands") {
|
|
497
|
+
return send(res, 200, { ok: true, commands: loadCommands().slice(-100) });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (req.method === "POST" && p === "/ops/api/commands") {
|
|
501
|
+
let body;
|
|
502
|
+
try {
|
|
503
|
+
body = JSON.parse(await readBody(req));
|
|
504
|
+
} catch {
|
|
505
|
+
return send(res, 400, { ok: false });
|
|
506
|
+
}
|
|
507
|
+
const installId = sanitize(body.installId, "");
|
|
508
|
+
const command = String(body.command || "");
|
|
509
|
+
if (!installId || !ALLOWED_REMOTE.has(command)) {
|
|
510
|
+
return send(res, 400, { ok: false, error: "invalid command or install" });
|
|
511
|
+
}
|
|
512
|
+
const cmd = {
|
|
513
|
+
id: crypto.randomUUID(),
|
|
514
|
+
installId,
|
|
515
|
+
command,
|
|
516
|
+
args: body.args && typeof body.args === "object" ? body.args : {},
|
|
517
|
+
status: "pending",
|
|
518
|
+
createdAt: new Date().toISOString(),
|
|
519
|
+
createdBy: "ops",
|
|
520
|
+
};
|
|
521
|
+
const list = loadCommands();
|
|
522
|
+
list.push(cmd);
|
|
523
|
+
saveCommands(list);
|
|
524
|
+
audit(`cmd-enqueue id=${cmd.id} cmd=${command} install=${installId}`);
|
|
525
|
+
return send(res, 200, { ok: true, command: cmd });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (req.method === "GET" && p === "/ops/api/health") {
|
|
529
|
+
return send(res, 200, {
|
|
530
|
+
ok: true,
|
|
531
|
+
service: "fleet-collector",
|
|
532
|
+
installs: Object.keys(loadInstalls()).length,
|
|
533
|
+
crashes: listCrashes(1000).length,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return send(res, 404, { ok: false });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const server = http.createServer(async (req, res) => {
|
|
541
|
+
try {
|
|
542
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${PORT}`);
|
|
543
|
+
const ingestSub = parseIngestPath(url.pathname);
|
|
544
|
+
if (ingestSub !== null) {
|
|
545
|
+
return await handleIngest(req, res, ingestSub, url);
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
url.pathname === "/" ||
|
|
549
|
+
url.pathname.startsWith("/ops")
|
|
550
|
+
) {
|
|
551
|
+
return await handleOps(req, res, url);
|
|
552
|
+
}
|
|
553
|
+
return send(res, 404, { ok: false });
|
|
554
|
+
} catch (e) {
|
|
555
|
+
console.error("[fleet] error", e);
|
|
556
|
+
if (!res.writableEnded) send(res, 500, { ok: false });
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
561
|
+
console.log(
|
|
562
|
+
`[fleet] écoute 127.0.0.1:${PORT} data=${DATA_DIR} title=${UI.title} ingest=/i-${INGEST.slice(0, 8)}…`,
|
|
563
|
+
);
|
|
564
|
+
});
|