@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,696 @@
|
|
|
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>Creezio Server Admin</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #14182f;
|
|
10
|
+
--bg-soft: #1b2140;
|
|
11
|
+
--card: #1e2447;
|
|
12
|
+
--line: #2c3462;
|
|
13
|
+
--fg: #e6e9f5;
|
|
14
|
+
--muted: #8a93b8;
|
|
15
|
+
--accent: #f0701d;
|
|
16
|
+
--accent-soft: rgba(240,112,29,.14);
|
|
17
|
+
--ok: #34d399;
|
|
18
|
+
--warn: #fbbf24;
|
|
19
|
+
--bad: #f87171;
|
|
20
|
+
--radius: 12px;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
html, body { margin: 0; min-height: 100%; }
|
|
24
|
+
body {
|
|
25
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
26
|
+
background: var(--bg); color: var(--fg);
|
|
27
|
+
padding: 0 0 60px;
|
|
28
|
+
}
|
|
29
|
+
a { color: var(--accent); text-decoration: none; }
|
|
30
|
+
a:hover { text-decoration: underline; }
|
|
31
|
+
button { font: inherit; cursor: pointer; }
|
|
32
|
+
|
|
33
|
+
.topbar {
|
|
34
|
+
display: flex; align-items: center; gap: 14px;
|
|
35
|
+
padding: 18px 28px; border-bottom: 1px solid var(--line);
|
|
36
|
+
background: rgba(20,24,47,.9); position: sticky; top: 0; z-index: 5;
|
|
37
|
+
backdrop-filter: blur(8px);
|
|
38
|
+
}
|
|
39
|
+
.mark {
|
|
40
|
+
width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
|
|
41
|
+
background: linear-gradient(135deg, #f0701d, #c24e08);
|
|
42
|
+
display: grid; place-items: center; font-weight: 700; font-size: 13px; color: #fff;
|
|
43
|
+
}
|
|
44
|
+
.topbar h1 { margin: 0; font-size: 16px; font-weight: 650; letter-spacing: -.01em; }
|
|
45
|
+
.topbar .sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
|
|
46
|
+
.topbar .spacer { flex: 1; }
|
|
47
|
+
.pill {
|
|
48
|
+
font-size: 12px; padding: 4px 10px; border-radius: 999px;
|
|
49
|
+
border: 1px solid var(--line); color: var(--muted); white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
.pill.ok { color: var(--ok); border-color: rgba(52,211,153,.35); }
|
|
52
|
+
.pill.bad { color: var(--bad); border-color: rgba(248,113,113,.35); }
|
|
53
|
+
|
|
54
|
+
.wrap { max-width: 1080px; margin: 0 auto; padding: 24px 28px; }
|
|
55
|
+
.section-title {
|
|
56
|
+
font-size: 12px; font-weight: 600; letter-spacing: .08em;
|
|
57
|
+
text-transform: uppercase; color: var(--muted); margin: 26px 0 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.card {
|
|
61
|
+
background: var(--card); border: 1px solid var(--line);
|
|
62
|
+
border-radius: var(--radius); padding: 16px 18px; margin-bottom: 14px;
|
|
63
|
+
}
|
|
64
|
+
.srv-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
65
|
+
.srv-name { font-size: 15px; font-weight: 650; }
|
|
66
|
+
.srv-meta { font-size: 12px; color: var(--muted); }
|
|
67
|
+
.badge {
|
|
68
|
+
font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
|
|
69
|
+
text-transform: uppercase; letter-spacing: .04em;
|
|
70
|
+
}
|
|
71
|
+
.badge.running { background: rgba(52,211,153,.15); color: var(--ok); }
|
|
72
|
+
.badge.exited, .badge.created, .badge.paused { background: rgba(251,191,36,.14); color: var(--warn); }
|
|
73
|
+
.badge.absent, .badge.unknown, .badge.dead { background: rgba(248,113,113,.14); color: var(--bad); }
|
|
74
|
+
.badge.orphan { background: var(--accent-soft); color: var(--accent); }
|
|
75
|
+
.health { font-size: 11px; color: var(--muted); }
|
|
76
|
+
.health.healthy { color: var(--ok); }
|
|
77
|
+
.health.unhealthy { color: var(--bad); }
|
|
78
|
+
.srv-actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
79
|
+
.btn {
|
|
80
|
+
background: var(--bg-soft); border: 1px solid var(--line); color: var(--fg);
|
|
81
|
+
padding: 6px 12px; border-radius: 8px; font-size: 12.5px;
|
|
82
|
+
}
|
|
83
|
+
.btn:hover { border-color: var(--accent); }
|
|
84
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
|
|
85
|
+
.btn.primary:hover { filter: brightness(1.08); }
|
|
86
|
+
.btn.danger:hover { border-color: var(--bad); color: var(--bad); }
|
|
87
|
+
.btn:disabled { opacity: .5; cursor: default; }
|
|
88
|
+
.srv-disk { font-size: 11.5px; color: var(--muted); margin-top: 8px; }
|
|
89
|
+
|
|
90
|
+
/* Boot splash */
|
|
91
|
+
.boot {
|
|
92
|
+
margin-top: 12px; padding: 14px 16px; border-radius: 10px;
|
|
93
|
+
background: var(--bg); border: 1px solid var(--line);
|
|
94
|
+
}
|
|
95
|
+
.boot-head { display: flex; align-items: baseline; gap: 10px; }
|
|
96
|
+
.boot-head .headline { font-size: 13px; font-weight: 600; }
|
|
97
|
+
.boot-head .pct { margin-left: auto; font-size: 13px; color: var(--accent); font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
98
|
+
.boot-bar {
|
|
99
|
+
height: 6px; border-radius: 999px; background: var(--bg-soft);
|
|
100
|
+
overflow: hidden; margin: 10px 0 12px;
|
|
101
|
+
}
|
|
102
|
+
.boot-bar > div {
|
|
103
|
+
height: 100%; border-radius: 999px; width: 0%;
|
|
104
|
+
background: linear-gradient(90deg, #f0701d, #ff9a4d);
|
|
105
|
+
transition: width .5s ease;
|
|
106
|
+
}
|
|
107
|
+
.boot-steps { display: grid; gap: 4px; font-size: 12px; }
|
|
108
|
+
.boot-step { display: flex; gap: 8px; align-items: baseline; color: var(--muted); }
|
|
109
|
+
.boot-step .ico { width: 14px; text-align: center; flex-shrink: 0; }
|
|
110
|
+
.boot-step.running { color: var(--fg); }
|
|
111
|
+
.boot-step.running .ico { color: var(--accent); }
|
|
112
|
+
.boot-step.done .ico { color: var(--ok); }
|
|
113
|
+
.boot-step.error { color: var(--bad); }
|
|
114
|
+
.boot-step .detail { color: var(--muted); font-size: 11px; }
|
|
115
|
+
.boot-footer { margin-top: 8px; font-size: 11px; color: var(--muted); }
|
|
116
|
+
|
|
117
|
+
/* Panels logs/ops */
|
|
118
|
+
.panel {
|
|
119
|
+
margin-top: 12px; border: 1px solid var(--line); border-radius: 10px;
|
|
120
|
+
background: #0e1226; overflow: hidden;
|
|
121
|
+
}
|
|
122
|
+
.panel-head {
|
|
123
|
+
display: flex; align-items: center; gap: 8px;
|
|
124
|
+
padding: 8px 12px; border-bottom: 1px solid var(--line);
|
|
125
|
+
font-size: 12px; color: var(--muted);
|
|
126
|
+
}
|
|
127
|
+
.panel-head .spacer { flex: 1; }
|
|
128
|
+
.panel pre {
|
|
129
|
+
margin: 0; padding: 12px; font-size: 11.5px; line-height: 1.5;
|
|
130
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
131
|
+
max-height: 320px; overflow: auto; white-space: pre-wrap; word-break: break-all;
|
|
132
|
+
color: #b8c0e0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Formulaire */
|
|
136
|
+
.form-grid { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
|
137
|
+
.field { display: flex; flex-direction: column; gap: 5px; }
|
|
138
|
+
.field label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
|
|
139
|
+
.field input, .field select {
|
|
140
|
+
background: var(--bg-soft); border: 1px solid var(--line); color: var(--fg);
|
|
141
|
+
padding: 8px 10px; border-radius: 8px; font: inherit; font-size: 13px; min-width: 160px;
|
|
142
|
+
}
|
|
143
|
+
.field input:focus, .field select:focus { outline: none; border-color: var(--accent); }
|
|
144
|
+
.form-msg { font-size: 12.5px; margin-top: 10px; }
|
|
145
|
+
.form-msg.err { color: var(--bad); }
|
|
146
|
+
.form-msg.ok { color: var(--ok); }
|
|
147
|
+
.empty { color: var(--muted); font-size: 13.5px; padding: 20px 4px; }
|
|
148
|
+
.disk-global { font-size: 12px; color: var(--muted); }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="topbar">
|
|
153
|
+
<div class="mark">CZ</div>
|
|
154
|
+
<div>
|
|
155
|
+
<h1>Creezio Server Admin</h1>
|
|
156
|
+
<div class="sub">Serveurs marque headless — Docker</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="spacer"></div>
|
|
159
|
+
<span class="pill" id="pillDocker">docker : …</span>
|
|
160
|
+
<span class="pill" id="pillDisk">disque : …</span>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="wrap">
|
|
164
|
+
<div class="section-title">Hôtes flotte (VPS enrôlés)</div>
|
|
165
|
+
<div class="card">
|
|
166
|
+
<div id="hosts"><div class="empty">Chargement…</div></div>
|
|
167
|
+
<div class="form-grid" style="margin-top:10px">
|
|
168
|
+
<div class="field">
|
|
169
|
+
<label for="fHostLabel">Label du prochain VPS</label>
|
|
170
|
+
<input id="fHostLabel" placeholder="resto-lyon-1"/>
|
|
171
|
+
</div>
|
|
172
|
+
<button class="btn" id="btnEnrollToken">Générer un token d'enrôlement</button>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="form-msg" id="hostMsg"></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="section-title">Nouveau serveur (hôte local)</div>
|
|
178
|
+
<div class="card">
|
|
179
|
+
<div class="form-grid">
|
|
180
|
+
<div class="field">
|
|
181
|
+
<label for="fBrand">Marque</label>
|
|
182
|
+
<select id="fBrand"></select>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="field">
|
|
185
|
+
<label for="fName">Nom</label>
|
|
186
|
+
<input id="fName" placeholder="demo" pattern="[a-z0-9][a-z0-9-]{0,30}"/>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="field">
|
|
189
|
+
<label for="fPort">Port (optionnel)</label>
|
|
190
|
+
<input id="fPort" placeholder="auto" inputmode="numeric" style="min-width:110px"/>
|
|
191
|
+
</div>
|
|
192
|
+
<button class="btn primary" id="btnCreate">Créer + démarrer</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="form-msg" id="formMsg"></div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="section-title" style="display:flex;align-items:center;gap:10px">
|
|
198
|
+
<span>Serveurs</span>
|
|
199
|
+
<span class="spacer" style="flex:1"></span>
|
|
200
|
+
<button class="btn" id="btnUpdateAll">Tout mettre à jour…</button>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="form-msg" id="updateAllMsg"></div>
|
|
203
|
+
<div id="servers"><div class="empty">Chargement…</div></div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<script>
|
|
207
|
+
"use strict";
|
|
208
|
+
const $ = (sel, root) => (root || document).querySelector(sel);
|
|
209
|
+
|
|
210
|
+
const state = {
|
|
211
|
+
servers: [],
|
|
212
|
+
hosts: [],
|
|
213
|
+
brandRoots: [],
|
|
214
|
+
disk: null,
|
|
215
|
+
openPanels: {}, // key → "logs"|"ops"
|
|
216
|
+
bootPollers: {}, // key → true (poll actif)
|
|
217
|
+
bootModels: {}, // key → dernier boot-status détaillé
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const keyOf = (s) => `${s.hostId || "local"}/${s.brandId}/${s.name}`;
|
|
221
|
+
const isLocal = (s) => !s.hostId || s.hostId === "local";
|
|
222
|
+
const apiBase = (s) =>
|
|
223
|
+
isLocal(s)
|
|
224
|
+
? `/admin/api/servers/${encodeURIComponent(s.brandId)}/${encodeURIComponent(s.name)}`
|
|
225
|
+
: `/admin/api/hosts/${encodeURIComponent(s.hostId)}/servers/${encodeURIComponent(s.brandId)}/${encodeURIComponent(s.name)}`;
|
|
226
|
+
|
|
227
|
+
function fmtBytes(n) {
|
|
228
|
+
if (n === null || n === undefined) return "?";
|
|
229
|
+
const u = ["o", "Ko", "Mo", "Go", "To"];
|
|
230
|
+
let i = 0, v = n;
|
|
231
|
+
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
|
232
|
+
return `${v >= 10 || i === 0 ? Math.round(v) : v.toFixed(1)} ${u[i]}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function api(path, opts) {
|
|
236
|
+
const res = await fetch(path, opts);
|
|
237
|
+
let json = null;
|
|
238
|
+
try { json = await res.json(); } catch { /* vide */ }
|
|
239
|
+
return { status: res.status, json };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ------------------------------------------------------------ rendering */
|
|
243
|
+
|
|
244
|
+
function stepIcon(status) {
|
|
245
|
+
if (status === "done") return "✓";
|
|
246
|
+
if (status === "running") return "●";
|
|
247
|
+
if (status === "error") return "!";
|
|
248
|
+
if (status === "skip") return "–";
|
|
249
|
+
return "○";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderBoot(key) {
|
|
253
|
+
const model = state.bootModels[key];
|
|
254
|
+
if (!model) return "";
|
|
255
|
+
const pct = Math.max(0, Math.min(100, Math.round(model.overallPercent || 0)));
|
|
256
|
+
const steps = (model.steps || []).map((st) => `
|
|
257
|
+
<div class="boot-step ${st.status}">
|
|
258
|
+
<span class="ico">${stepIcon(st.status)}</span>
|
|
259
|
+
<span>${escapeHtml(st.label || st.id)}</span>
|
|
260
|
+
${st.detail ? `<span class="detail">${escapeHtml(st.detail)}</span>` : ""}
|
|
261
|
+
</div>`).join("");
|
|
262
|
+
return `
|
|
263
|
+
<div class="boot">
|
|
264
|
+
<div class="boot-head">
|
|
265
|
+
<span class="headline">${escapeHtml(model.headline || "Démarrage…")}</span>
|
|
266
|
+
<span class="pct">${pct}%</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="boot-bar"><div style="width:${pct}%"></div></div>
|
|
269
|
+
<div class="boot-steps">${steps}</div>
|
|
270
|
+
${model.footer ? `<div class="boot-footer">${escapeHtml(model.footer)}</div>` : ""}
|
|
271
|
+
</div>`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function escapeHtml(s) {
|
|
275
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) =>
|
|
276
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function diskOf(s) {
|
|
280
|
+
if (!state.disk || !isLocal(s)) return null;
|
|
281
|
+
return (state.disk.instances || []).find(
|
|
282
|
+
(d) => d.brandId === s.brandId && d.name === s.name,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function renderHosts() {
|
|
287
|
+
const root = $("#hosts");
|
|
288
|
+
if (!state.hosts.length) {
|
|
289
|
+
root.innerHTML = `<div class="empty">Aucun VPS enrôlé — générer un token puis, sur le VPS restaurant :<br/>
|
|
290
|
+
<code>creezio server-docker enroll --brand-root . --admin <URL admin> --token <token> --slug <slug></code></div>`;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
root.innerHTML = state.hosts.map((h) => `
|
|
294
|
+
<div class="srv-head" style="padding:6px 0">
|
|
295
|
+
<span class="srv-name">${escapeHtml(h.label || h.hostId)}</span>
|
|
296
|
+
<span class="srv-meta">${escapeHtml(h.agentUrl || "")}</span>
|
|
297
|
+
<span class="badge ${h.online ? "running" : "absent"}">${h.online ? "online" : "offline"}</span>
|
|
298
|
+
<span class="srv-meta">${h.lastSeen ? `vu ${escapeHtml(new Date(h.lastSeen).toLocaleString())}` : "jamais vu"}</span>
|
|
299
|
+
<div class="srv-actions">
|
|
300
|
+
<button class="btn danger" data-host-act="rm" data-host-id="${escapeHtml(h.hostId)}">Retirer</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>`).join("");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderServer(s) {
|
|
306
|
+
const key = keyOf(s);
|
|
307
|
+
const st = s.docker?.state || "unknown";
|
|
308
|
+
const running = st === "running";
|
|
309
|
+
const health = s.docker?.health;
|
|
310
|
+
const d = diskOf(s);
|
|
311
|
+
const panel = state.openPanels[key];
|
|
312
|
+
const crmUrl = crmUrlOf(s, running);
|
|
313
|
+
return `
|
|
314
|
+
<div class="card" data-key="${escapeHtml(key)}">
|
|
315
|
+
<div class="srv-head">
|
|
316
|
+
<span class="srv-name">${escapeHtml(s.name)}</span>
|
|
317
|
+
<span class="srv-meta">${escapeHtml(s.brandId || "?")}${s.port ? ` · :${s.port}` : ""}</span>
|
|
318
|
+
${!isLocal(s) ? `<span class="badge orphan">${escapeHtml(s.hostLabel || s.hostId)}</span>` : ""}
|
|
319
|
+
<span class="badge ${escapeHtml(st)}">${escapeHtml(st)}</span>
|
|
320
|
+
${health ? `<span class="health ${escapeHtml(health)}">${escapeHtml(health)}</span>` : ""}
|
|
321
|
+
${s.version ? `<span class="srv-meta">v${escapeHtml(s.version)}</span>` : ""}
|
|
322
|
+
${s.image ? `<span class="srv-meta" title="image">${escapeHtml(s.image)}</span>` : ""}
|
|
323
|
+
${s.orphan ? `<span class="badge orphan">orphelin</span>` : ""}
|
|
324
|
+
${crmUrl ? `<a href="${escapeHtml(crmUrl)}" target="_blank" rel="noopener">ouvrir le CRM ↗</a>` : ""}
|
|
325
|
+
<div class="srv-actions">
|
|
326
|
+
${s.orphan ? "" : `
|
|
327
|
+
<button class="btn" data-act="start" ${running ? "disabled" : ""}>Start</button>
|
|
328
|
+
<button class="btn" data-act="stop" ${running ? "" : "disabled"}>Stop</button>
|
|
329
|
+
<button class="btn" data-act="update">Mettre à jour</button>
|
|
330
|
+
<button class="btn" data-act="logs">Logs</button>
|
|
331
|
+
<button class="btn" data-act="ops">Ops</button>
|
|
332
|
+
<button class="btn danger" data-act="rm">Supprimer</button>`}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
${d ? `<div class="srv-disk">données : ${fmtBytes(d.sizeBytes)} (${escapeHtml(d.dataDir || "")})</div>` : ""}
|
|
336
|
+
${renderBoot(key)}
|
|
337
|
+
${panel ? `
|
|
338
|
+
<div class="panel">
|
|
339
|
+
<div class="panel-head">
|
|
340
|
+
<span>${panel === "logs" ? "docker logs (tail 200)" : "événements ops (JSONL)"}</span>
|
|
341
|
+
<span class="spacer"></span>
|
|
342
|
+
<button class="btn" data-act="refresh-panel">Rafraîchir</button>
|
|
343
|
+
<button class="btn" data-act="close-panel">Fermer</button>
|
|
344
|
+
</div>
|
|
345
|
+
<pre id="panel-${escapeHtml(key).replace(/[^a-z0-9]/gi, "_")}">Chargement…</pre>
|
|
346
|
+
</div>` : ""}
|
|
347
|
+
</div>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderAll() {
|
|
351
|
+
const root = $("#servers");
|
|
352
|
+
if (!state.servers.length) {
|
|
353
|
+
root.innerHTML = `<div class="empty">Aucun serveur — créer une instance ci-dessus.</div>`;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
root.innerHTML = state.servers.map(renderServer).join("");
|
|
357
|
+
for (const s of state.servers) {
|
|
358
|
+
const key = keyOf(s);
|
|
359
|
+
if (state.openPanels[key]) loadPanel(s, state.openPanels[key]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ------------------------------------------------------------ actions */
|
|
364
|
+
|
|
365
|
+
function crmUrlOf(s, running) {
|
|
366
|
+
if (!running) return null;
|
|
367
|
+
if (isLocal(s)) return s.port ? `http://127.0.0.1:${s.port}/` : null;
|
|
368
|
+
// Serveur distant : URL publique tunnel dérivée du slug (env persisté).
|
|
369
|
+
const slug = s.env && s.env.CREEZIO_TUNNEL_SLUG;
|
|
370
|
+
const host = state.hosts.find((h) => h.hostId === s.hostId);
|
|
371
|
+
const agentUrl = host?.agentUrl || "";
|
|
372
|
+
const m = agentUrl.match(/^https:\/\/agent\.(.+)$/);
|
|
373
|
+
if (slug && m) {
|
|
374
|
+
// agent.{slug}.{zone} → {slug}.{zone} ; slug de l'instance si différent.
|
|
375
|
+
const rest = m[1];
|
|
376
|
+
const zone = rest.split(".").slice(1).join(".");
|
|
377
|
+
return `https://${slug}.${zone || rest}/`;
|
|
378
|
+
}
|
|
379
|
+
if (m) return `https://${m[1]}/`;
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function loadPanel(s, kind) {
|
|
384
|
+
const key = keyOf(s);
|
|
385
|
+
const el = document.getElementById(`panel-${key.replace(/[^a-z0-9]/gi, "_")}`);
|
|
386
|
+
if (!el) return;
|
|
387
|
+
const base = apiBase(s);
|
|
388
|
+
if (kind === "logs") {
|
|
389
|
+
const r = await api(`${base}/logs?tail=200`);
|
|
390
|
+
el.textContent = r.json?.ok
|
|
391
|
+
? (r.json.lines || []).join("\n") || "(vide)"
|
|
392
|
+
: `erreur: ${r.json?.error || r.status}`;
|
|
393
|
+
} else {
|
|
394
|
+
const r = await api(`${base}/ops?limit=100`);
|
|
395
|
+
const ev = r.json?.events || [];
|
|
396
|
+
el.textContent = ev.length
|
|
397
|
+
? ev.map((e) => JSON.stringify(e)).join("\n")
|
|
398
|
+
: "(aucun événement ops)";
|
|
399
|
+
}
|
|
400
|
+
el.scrollTop = el.scrollHeight;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Update en masse : même image cible pour tous les serveurs d'une marque
|
|
405
|
+
* (hôtes locaux + enrôlés), séquentiel — un resto à la fois, on s'arrête
|
|
406
|
+
* de compter les échecs mais on continue la file.
|
|
407
|
+
*/
|
|
408
|
+
async function updateAll() {
|
|
409
|
+
const msg = $("#updateAllMsg");
|
|
410
|
+
const candidates = state.servers.filter((s) => !s.orphan);
|
|
411
|
+
if (!candidates.length) return alert("Aucun serveur à mettre à jour.");
|
|
412
|
+
const brands = [...new Set(candidates.map((s) => s.brandId))];
|
|
413
|
+
const brand = brands.length === 1
|
|
414
|
+
? brands[0]
|
|
415
|
+
: prompt(`Marque à mettre à jour (${brands.join(", ")}) :`, brands[0]);
|
|
416
|
+
if (!brand) return;
|
|
417
|
+
const targets = candidates.filter((s) => s.brandId === brand);
|
|
418
|
+
const sample = targets.find((s) => (s.image || "").includes(":"));
|
|
419
|
+
const input = prompt(
|
|
420
|
+
`Mettre à jour ${targets.length} serveur(s) « ${brand} »\n\nImage actuelle (exemple) : ${sample?.image || "?"}\n\nEntrer un TAG (ex. 0.2.1) ou une image complète :`,
|
|
421
|
+
);
|
|
422
|
+
if (!input) return;
|
|
423
|
+
let image = input.trim();
|
|
424
|
+
if (!image.includes("/") && !image.includes(":") && sample?.image?.includes(":")) {
|
|
425
|
+
image = `${sample.image.slice(0, sample.image.lastIndexOf(":"))}:${image}`;
|
|
426
|
+
}
|
|
427
|
+
const todo = targets.filter((s) => s.image !== image);
|
|
428
|
+
if (!todo.length) return alert(`Tous les serveurs « ${brand} » sont déjà sur :\n${image}`);
|
|
429
|
+
if (!confirm(`Mettre à jour ${todo.length}/${targets.length} serveur(s) vers :\n${image}\n\n(séquentiel — backup /data + rollback automatique par serveur)`)) return;
|
|
430
|
+
const results = [];
|
|
431
|
+
for (const s of todo) {
|
|
432
|
+
const key = keyOf(s);
|
|
433
|
+
msg.className = "form-msg";
|
|
434
|
+
msg.textContent = `Update ${results.length + 1}/${todo.length} : ${key}…`;
|
|
435
|
+
const base = apiBase(s);
|
|
436
|
+
const r = await api(`${base}/update`, {
|
|
437
|
+
method: "POST",
|
|
438
|
+
headers: { "Content-Type": "application/json" },
|
|
439
|
+
body: JSON.stringify({ image }),
|
|
440
|
+
});
|
|
441
|
+
if (!r.json?.ok) {
|
|
442
|
+
results.push(`✗ ${key} : refusé (${r.json?.error || r.status})`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
startBootPoll(s);
|
|
446
|
+
const final = await pollUpdateStatus(base);
|
|
447
|
+
const out = final?.result || {};
|
|
448
|
+
results.push(
|
|
449
|
+
final?.status === "done"
|
|
450
|
+
? `✓ ${key} → ${out.version || image}`
|
|
451
|
+
: `✗ ${key} : ${out.error || final?.status}${out.rolledBack ? " (rollback OK)" : ""}`,
|
|
452
|
+
);
|
|
453
|
+
await refresh();
|
|
454
|
+
}
|
|
455
|
+
const failed = results.filter((l) => l.startsWith("✗")).length;
|
|
456
|
+
msg.className = `form-msg ${failed ? "err" : "ok"}`;
|
|
457
|
+
msg.textContent = `Update en masse terminé — ${todo.length - failed}/${todo.length} OK`;
|
|
458
|
+
alert(`Update en masse terminé :\n\n${results.join("\n")}`);
|
|
459
|
+
await refresh();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Poll de l'update asynchrone (agent ou local) jusqu'à done/error. */
|
|
463
|
+
async function pollUpdateStatus(base, timeoutMs = 15 * 60 * 1000) {
|
|
464
|
+
const t0 = Date.now();
|
|
465
|
+
for (;;) {
|
|
466
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
467
|
+
try {
|
|
468
|
+
const r = await api(`${base}/update-status`);
|
|
469
|
+
const u = r.json?.update;
|
|
470
|
+
if (u && u.status !== "running") return u;
|
|
471
|
+
} catch {
|
|
472
|
+
/* agent momentanément injoignable pendant le recreate */
|
|
473
|
+
}
|
|
474
|
+
if (Date.now() - t0 > timeoutMs) {
|
|
475
|
+
return { status: "error", result: { error: "timeout suivi update" } };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function doAction(s, act) {
|
|
481
|
+
const key = keyOf(s);
|
|
482
|
+
const base = apiBase(s);
|
|
483
|
+
if (act === "start") {
|
|
484
|
+
await api(`${base}/start`, { method: "POST" });
|
|
485
|
+
startBootPoll(s);
|
|
486
|
+
} else if (act === "update") {
|
|
487
|
+
const current = s.image || "";
|
|
488
|
+
const input = prompt(
|
|
489
|
+
`Mettre à jour ${key}\n\nImage actuelle : ${current}\nVersion actuelle : ${s.version || "?"}\n\nEntrer un TAG (ex. 0.2.0) ou une image complète :`,
|
|
490
|
+
);
|
|
491
|
+
if (!input) return;
|
|
492
|
+
let image = input.trim();
|
|
493
|
+
if (!image.includes("/") && !image.includes(":") && current.includes(":")) {
|
|
494
|
+
image = `${current.slice(0, current.lastIndexOf(":"))}:${image}`;
|
|
495
|
+
}
|
|
496
|
+
if (!confirm(`Recréer ${key} avec :\n${image}\n\n(backup /data automatique, rollback si santé KO)`)) return;
|
|
497
|
+
// Update asynchrone (202) : le pull+backup+recreate+boot dépasse la
|
|
498
|
+
// durée max d'une requête proxifiée — on suit via /update-status.
|
|
499
|
+
const r = await api(`${base}/update`, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: { "Content-Type": "application/json" },
|
|
502
|
+
body: JSON.stringify({ image }),
|
|
503
|
+
});
|
|
504
|
+
if (!r.json?.ok) {
|
|
505
|
+
alert(`Update refusé : ${r.json?.error || r.status}`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
startBootPoll(s);
|
|
509
|
+
const final = await pollUpdateStatus(base);
|
|
510
|
+
const out = final?.result || {};
|
|
511
|
+
alert(
|
|
512
|
+
final?.status === "done"
|
|
513
|
+
? `Update OK → ${out.image || image} (version ${out.version || "?"})`
|
|
514
|
+
: `Update KO : ${out.error || final?.status || "?"}${out.rolledBack ? "\nRollback effectué → " + out.previousImage : ""}`,
|
|
515
|
+
);
|
|
516
|
+
startBootPoll(s);
|
|
517
|
+
} else if (act === "stop") {
|
|
518
|
+
await api(`${base}/stop`, { method: "POST" });
|
|
519
|
+
delete state.bootModels[key];
|
|
520
|
+
} else if (act === "rm") {
|
|
521
|
+
const purge = confirm(
|
|
522
|
+
`Supprimer le serveur ${key} ?\n\nOK = container supprimé, données conservées.\n(Une seconde confirmation propose la purge des données.)`,
|
|
523
|
+
);
|
|
524
|
+
if (!purge) return;
|
|
525
|
+
const purgeData = confirm("Supprimer AUSSI le dossier de données ? (irréversible)");
|
|
526
|
+
await api(`${base}?purgeData=${purgeData ? 1 : 0}`, { method: "DELETE" });
|
|
527
|
+
delete state.bootModels[key];
|
|
528
|
+
delete state.openPanels[key];
|
|
529
|
+
} else if (act === "logs" || act === "ops") {
|
|
530
|
+
state.openPanels[key] = state.openPanels[key] === act ? undefined : act;
|
|
531
|
+
} else if (act === "close-panel") {
|
|
532
|
+
delete state.openPanels[key];
|
|
533
|
+
} else if (act === "refresh-panel") {
|
|
534
|
+
renderAll();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await refresh();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
$("#servers").addEventListener("click", (ev) => {
|
|
541
|
+
const btn = ev.target.closest("button[data-act]");
|
|
542
|
+
if (!btn) return;
|
|
543
|
+
const card = ev.target.closest("[data-key]");
|
|
544
|
+
const s = state.servers.find((x) => keyOf(x) === card?.dataset.key);
|
|
545
|
+
if (s) doAction(s, btn.dataset.act);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
$("#hosts").addEventListener("click", async (ev) => {
|
|
549
|
+
const btn = ev.target.closest("button[data-host-act]");
|
|
550
|
+
if (!btn) return;
|
|
551
|
+
if (btn.dataset.hostAct === "rm") {
|
|
552
|
+
if (!confirm(`Retirer l'hôte ${btn.dataset.hostId} du registre flotte ?`)) return;
|
|
553
|
+
await api(`/admin/api/hosts/${encodeURIComponent(btn.dataset.hostId)}`, { method: "DELETE" });
|
|
554
|
+
await refresh();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
$("#btnUpdateAll").addEventListener("click", () => {
|
|
559
|
+
updateAll().catch((e) => alert(`Update en masse KO : ${e?.message || e}`));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
$("#btnEnrollToken").addEventListener("click", async () => {
|
|
563
|
+
const msg = $("#hostMsg");
|
|
564
|
+
msg.className = "form-msg";
|
|
565
|
+
const label = $("#fHostLabel").value.trim();
|
|
566
|
+
const r = await api("/admin/api/hosts/enroll-token", {
|
|
567
|
+
method: "POST",
|
|
568
|
+
headers: { "Content-Type": "application/json" },
|
|
569
|
+
body: JSON.stringify({ label }),
|
|
570
|
+
});
|
|
571
|
+
if (r.json?.ok) {
|
|
572
|
+
msg.className = "form-msg ok";
|
|
573
|
+
msg.innerHTML = `Token (affiché UNE fois) : <code>${escapeHtml(r.json.enrollToken)}</code><br/>
|
|
574
|
+
Sur le VPS restaurant : <code>creezio server-docker enroll --brand-root . --admin <URL admin> --token ${escapeHtml(r.json.enrollToken)} --slug <slug></code>`;
|
|
575
|
+
} else {
|
|
576
|
+
msg.className = "form-msg err";
|
|
577
|
+
msg.textContent = r.json?.error || `erreur HTTP ${r.status}`;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
/* --------------------------------------------------------- boot polling */
|
|
582
|
+
|
|
583
|
+
function startBootPoll(s) {
|
|
584
|
+
const key = keyOf(s);
|
|
585
|
+
if (state.bootPollers[key]) return;
|
|
586
|
+
state.bootPollers[key] = true;
|
|
587
|
+
const tick = async () => {
|
|
588
|
+
const r = await api(`${apiBase(s)}/boot-status`).catch(() => null);
|
|
589
|
+
const model = r && r.status === 200 ? r.json : null;
|
|
590
|
+
if (model) {
|
|
591
|
+
state.bootModels[key] = model;
|
|
592
|
+
renderAll();
|
|
593
|
+
const donePct = (model.overallPercent ?? 0) >= 100;
|
|
594
|
+
if (model.booting === false && donePct) {
|
|
595
|
+
delete state.bootPollers[key];
|
|
596
|
+
// Laisse la barre 100% visible 4s puis nettoie.
|
|
597
|
+
setTimeout(() => { delete state.bootModels[key]; refresh(); }, 4000);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (state.bootPollers[key]) setTimeout(tick, 2000);
|
|
602
|
+
};
|
|
603
|
+
tick();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* --------------------------------------------------------------- create */
|
|
607
|
+
|
|
608
|
+
$("#btnCreate").addEventListener("click", async () => {
|
|
609
|
+
const msg = $("#formMsg");
|
|
610
|
+
msg.className = "form-msg";
|
|
611
|
+
msg.textContent = "";
|
|
612
|
+
const brandRoot = $("#fBrand").value;
|
|
613
|
+
const name = $("#fName").value.trim();
|
|
614
|
+
const portRaw = $("#fPort").value.trim();
|
|
615
|
+
if (!brandRoot || !name) {
|
|
616
|
+
msg.className = "form-msg err";
|
|
617
|
+
msg.textContent = "Marque et nom requis.";
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const body = { brandRoot, name };
|
|
621
|
+
if (portRaw) body.port = Number(portRaw);
|
|
622
|
+
$("#btnCreate").disabled = true;
|
|
623
|
+
msg.textContent = "Création en cours…";
|
|
624
|
+
try {
|
|
625
|
+
const r = await api("/admin/api/servers", {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers: { "Content-Type": "application/json" },
|
|
628
|
+
body: JSON.stringify(body),
|
|
629
|
+
});
|
|
630
|
+
if (r.json?.ok) {
|
|
631
|
+
msg.className = "form-msg ok";
|
|
632
|
+
msg.textContent = `Serveur ${name} créé (port ${r.json.instance.port}) — boot en cours…`;
|
|
633
|
+
$("#fName").value = "";
|
|
634
|
+
$("#fPort").value = "";
|
|
635
|
+
await refresh();
|
|
636
|
+
startBootPoll({ hostId: "local", brandId: r.json.instance.brandId, name });
|
|
637
|
+
} else {
|
|
638
|
+
msg.className = "form-msg err";
|
|
639
|
+
msg.textContent = r.json?.error || `erreur HTTP ${r.status}`;
|
|
640
|
+
}
|
|
641
|
+
} finally {
|
|
642
|
+
$("#btnCreate").disabled = false;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
/* -------------------------------------------------------------- refresh */
|
|
647
|
+
|
|
648
|
+
async function refresh() {
|
|
649
|
+
const [srv, disk, hosts] = await Promise.all([
|
|
650
|
+
api("/admin/api/servers").catch(() => null),
|
|
651
|
+
api("/admin/api/disk").catch(() => null),
|
|
652
|
+
api("/admin/api/hosts").catch(() => null),
|
|
653
|
+
]);
|
|
654
|
+
if (hosts?.json?.ok) {
|
|
655
|
+
state.hosts = hosts.json.hosts || [];
|
|
656
|
+
renderHosts();
|
|
657
|
+
}
|
|
658
|
+
if (srv?.json?.ok) {
|
|
659
|
+
state.servers = srv.json.servers;
|
|
660
|
+
const pd = $("#pillDocker");
|
|
661
|
+
pd.textContent = `docker : ${srv.json.docker ? "ok" : "indisponible"}`;
|
|
662
|
+
pd.className = `pill ${srv.json.docker ? "ok" : "bad"}`;
|
|
663
|
+
// Boot polling automatique sur les serveurs en cours de boot.
|
|
664
|
+
for (const s of state.servers) {
|
|
665
|
+
const bs = s.bootStatus;
|
|
666
|
+
if (bs && (bs.booting || (bs.overallPercent ?? 100) < 100)) {
|
|
667
|
+
startBootPoll(s);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (disk?.json?.ok) {
|
|
672
|
+
state.disk = disk.json;
|
|
673
|
+
const f = disk.json.filesystem;
|
|
674
|
+
$("#pillDisk").textContent = f
|
|
675
|
+
? `disque : ${fmtBytes(f.freeBytes)} libres / ${fmtBytes(f.totalBytes)}`
|
|
676
|
+
: "disque : ?";
|
|
677
|
+
}
|
|
678
|
+
renderAll();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function init() {
|
|
682
|
+
const h = await api("/admin/api/health").catch(() => null);
|
|
683
|
+
state.brandRoots = h?.json?.brandRoots || [];
|
|
684
|
+
const sel = $("#fBrand");
|
|
685
|
+
sel.innerHTML = state.brandRoots.length
|
|
686
|
+
? state.brandRoots
|
|
687
|
+
.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r.split("/").pop() || r)} — ${escapeHtml(r)}</option>`)
|
|
688
|
+
.join("")
|
|
689
|
+
: `<option value="">(aucune marque — CREEZIO_ADMIN_BRAND_ROOTS)</option>`;
|
|
690
|
+
await refresh();
|
|
691
|
+
setInterval(refresh, 5000);
|
|
692
|
+
}
|
|
693
|
+
init();
|
|
694
|
+
</script>
|
|
695
|
+
</body>
|
|
696
|
+
</html>
|