@imenam/self-register-proxy 1.0.42 → 1.0.43
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/package.json +1 -1
- package/routes/metaConfig.js +2 -2
- package/views/dashboard.js +309 -803
- package/views/login.js +61 -130
- package/views/metaConfigs.js +403 -643
- package/views/terminal.html +272 -691
- package/views/terminal.js +34 -30
- package/views/theme.js +431 -0
package/views/terminal.js
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { escapeHtml } = require('../services/proxyRouter');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* @param {string}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { escapeHtml } = require('../services/proxyRouter');
|
|
4
|
+
const { BASE_CSS } = require('./theme');
|
|
5
|
+
|
|
6
|
+
const templatePath = path.join(__dirname, 'terminal.html');
|
|
7
|
+
let templateCache = null;
|
|
8
|
+
|
|
9
|
+
function loadTemplate() {
|
|
10
|
+
if (!templateCache) {
|
|
11
|
+
templateCache = fs.readFileSync(templatePath, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
return templateCache;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Page du terminal en ligne (xterm.js + WebSocket)
|
|
18
|
+
* @param {string} proxyName
|
|
19
|
+
* @param {string} terminalCommand
|
|
20
|
+
*/
|
|
21
|
+
function renderTerminal(proxyName, terminalCommand) {
|
|
22
|
+
const commandHtml = terminalCommand
|
|
23
|
+
? '<span class="terminal-command">' + escapeHtml(terminalCommand) + '</span>'
|
|
24
|
+
: '';
|
|
25
|
+
|
|
26
|
+
// Remplacements par fonction : le CSS et la commande peuvent contenir des `$`,
|
|
27
|
+
// qui seraient sinon interprétés comme des motifs de remplacement.
|
|
28
|
+
return loadTemplate()
|
|
29
|
+
.replace('{{BASE_CSS}}', () => BASE_CSS)
|
|
30
|
+
.replace('{{PROXY_NAME}}', () => escapeHtml(proxyName))
|
|
31
|
+
.replace('{{TERMINAL_COMMAND}}', () => commandHtml);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { renderTerminal };
|
package/views/theme.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
const { escapeHtml } = require('../services/proxyRouter');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Socle visuel partagé par toutes les pages du proxy.
|
|
5
|
+
* Un seul jeu de tokens, un seul accent, une seule échelle typographique.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Icônes Lucide inlinées — pas de CDN, colorées par `currentColor`. */
|
|
9
|
+
const ICONS = {
|
|
10
|
+
network: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
|
|
11
|
+
layers: '<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
|
|
12
|
+
terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
|
|
13
|
+
sliders: '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
|
|
14
|
+
trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
|
|
15
|
+
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>',
|
|
16
|
+
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
17
|
+
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
18
|
+
more: '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
|
|
19
|
+
activity: '<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>',
|
|
20
|
+
copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
|
21
|
+
circleX: '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
|
|
22
|
+
chevronDown: '<path d="m6 9 6 6 6-6"/>',
|
|
23
|
+
alert: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
24
|
+
circleAlert: '<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>',
|
|
25
|
+
lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
26
|
+
arrowLeft: '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
|
|
27
|
+
arrowUp: '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
|
|
28
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
29
|
+
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
30
|
+
refresh: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
|
31
|
+
externalLink: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
|
32
|
+
pencil: '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rend une icône SVG inline.
|
|
37
|
+
* @param {string} name - clé de ICONS
|
|
38
|
+
* @param {number} [size=16]
|
|
39
|
+
*/
|
|
40
|
+
function icon(name, size = 16) {
|
|
41
|
+
const body = ICONS[name];
|
|
42
|
+
if (!body) return '';
|
|
43
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Tokens + composants communs. Injecté dans le <style> de chaque page. */
|
|
47
|
+
const BASE_CSS = `
|
|
48
|
+
:root {
|
|
49
|
+
--bg: #0F0F12;
|
|
50
|
+
--sidebar: #131317;
|
|
51
|
+
--surface: #17171C;
|
|
52
|
+
--surface-2: #1C1C22;
|
|
53
|
+
--border: #26262E;
|
|
54
|
+
--border-subtle: #1F1F25;
|
|
55
|
+
--text: #F4F4F5;
|
|
56
|
+
--text-2: #A1A1AA;
|
|
57
|
+
--text-3: #71717A;
|
|
58
|
+
--text-4: #52525B;
|
|
59
|
+
--accent: #6366F1;
|
|
60
|
+
--accent-soft: #818CF8;
|
|
61
|
+
--green: #22C55E;
|
|
62
|
+
--amber: #F59E0B;
|
|
63
|
+
--red: #F87171;
|
|
64
|
+
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
65
|
+
--mono: ui-monospace, SFMono-Regular, Consolas, Menlo, monospace;
|
|
66
|
+
}
|
|
67
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
68
|
+
body {
|
|
69
|
+
font-family: var(--sans);
|
|
70
|
+
background: var(--bg);
|
|
71
|
+
color: var(--text);
|
|
72
|
+
min-height: 100vh;
|
|
73
|
+
-webkit-font-smoothing: antialiased;
|
|
74
|
+
}
|
|
75
|
+
a { color: inherit; text-decoration: none; }
|
|
76
|
+
::selection { background: rgba(99,102,241,.35); }
|
|
77
|
+
|
|
78
|
+
/* Boutons */
|
|
79
|
+
.btn {
|
|
80
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
81
|
+
height: 34px; padding: 0 14px; border-radius: 8px;
|
|
82
|
+
font-family: inherit; font-size: 13px; cursor: pointer;
|
|
83
|
+
border: 1px solid transparent; white-space: nowrap; transition: background .15s, border-color .15s;
|
|
84
|
+
}
|
|
85
|
+
.btn-primary { background: var(--accent); color: #fff; font-weight: 600; }
|
|
86
|
+
.btn-primary:hover { background: #5457E5; }
|
|
87
|
+
.btn-ghost { background: var(--surface); border-color: var(--border); color: var(--text); }
|
|
88
|
+
.btn-ghost:hover { border-color: #3A3A45; }
|
|
89
|
+
.btn-danger { background: rgba(239,68,68,.12); border-color: rgba(239,68,68,.3); color: var(--red); }
|
|
90
|
+
.btn-danger:hover { background: rgba(239,68,68,.2); }
|
|
91
|
+
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
92
|
+
.btn-link {
|
|
93
|
+
background: none; border: 0; font-family: inherit; font-size: 13px;
|
|
94
|
+
color: var(--text-3); cursor: pointer; padding: 0;
|
|
95
|
+
}
|
|
96
|
+
.btn-link:hover { color: var(--text-2); }
|
|
97
|
+
.btn-link.danger { color: var(--red); }
|
|
98
|
+
|
|
99
|
+
/* Champs */
|
|
100
|
+
.input, select, textarea {
|
|
101
|
+
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
|
102
|
+
border-radius: 8px; color: var(--text); font-family: inherit; font-size: 13px;
|
|
103
|
+
padding: 0 12px; height: 36px;
|
|
104
|
+
}
|
|
105
|
+
textarea { padding: 10px 12px; height: auto; resize: vertical; line-height: 1.5; }
|
|
106
|
+
.input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
|
|
107
|
+
.input::placeholder, textarea::placeholder { color: var(--text-3); }
|
|
108
|
+
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
|
109
|
+
.field label { font-size: 12px; color: var(--text-2); }
|
|
110
|
+
.field-inline { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
|
|
111
|
+
.field-inline input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }
|
|
112
|
+
.field-inline label { font-size: 13px; color: var(--text-2); }
|
|
113
|
+
.search { position: relative; }
|
|
114
|
+
.search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-3); pointer-events: none; }
|
|
115
|
+
.search .input { padding-left: 34px; }
|
|
116
|
+
|
|
117
|
+
/* En-tête de page */
|
|
118
|
+
.page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 26px; }
|
|
119
|
+
.page-title { font-size: 22px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
|
|
120
|
+
.page-sub { font-size: 13px; color: var(--text-3); margin-top: 5px; }
|
|
121
|
+
.page-actions { margin-left: auto; display: flex; gap: 12px; align-items: center; flex-shrink: 0; }
|
|
122
|
+
|
|
123
|
+
/* Listes */
|
|
124
|
+
.list-head {
|
|
125
|
+
display: flex; align-items: center; gap: 10px;
|
|
126
|
+
font-size: 12px; font-weight: 600; color: var(--text-2); margin-bottom: 8px;
|
|
127
|
+
}
|
|
128
|
+
.list-head .spacer { margin-left: auto; }
|
|
129
|
+
.list-head-meta { font-size: 12px; font-weight: 400; color: var(--text-4); }
|
|
130
|
+
.list { border-top: 1px solid var(--border-subtle); }
|
|
131
|
+
.row {
|
|
132
|
+
display: flex; align-items: center; gap: 16px;
|
|
133
|
+
min-height: 60px; padding: 8px; border-bottom: 1px solid var(--border-subtle);
|
|
134
|
+
border-radius: 10px; position: relative;
|
|
135
|
+
}
|
|
136
|
+
.row:hover { background: var(--surface); }
|
|
137
|
+
.row-main { min-width: 0; flex: 1 1 auto; }
|
|
138
|
+
.row-name { font-size: 15px; font-weight: 600; color: var(--text); }
|
|
139
|
+
.row-meta {
|
|
140
|
+
font-size: 12px; color: var(--text-4); font-family: var(--mono);
|
|
141
|
+
margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
.row-open { font-size: 13px; font-weight: 500; color: var(--accent-soft); }
|
|
144
|
+
.row-open:hover { color: #A5B4FC; }
|
|
145
|
+
|
|
146
|
+
/* Pastilles, badges, étiquettes */
|
|
147
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; background: #3F3F46; }
|
|
148
|
+
.dot.up { background: var(--green); }
|
|
149
|
+
.dot.warn { background: var(--amber); }
|
|
150
|
+
.dot.down { background: var(--red); }
|
|
151
|
+
.badge { font-size: 11px; padding: 2px 10px; border-radius: 999px; border: 1px solid; }
|
|
152
|
+
.badge-green { color: #4ADE80; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.3); }
|
|
153
|
+
.tag {
|
|
154
|
+
font-size: 11px; color: var(--text-2); background: var(--surface-2);
|
|
155
|
+
border: 1px solid var(--border); border-radius: 6px; padding: 3px 10px; white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
.tag-muted { color: var(--text-3); }
|
|
158
|
+
|
|
159
|
+
/* Menu contextuel */
|
|
160
|
+
.menu-wrap { position: relative; flex-shrink: 0; }
|
|
161
|
+
.icon-btn {
|
|
162
|
+
display: grid; place-items: center; width: 28px; height: 28px;
|
|
163
|
+
border-radius: 6px; background: none; border: 0; color: var(--text-3); cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
.icon-btn:hover { background: var(--surface-2); color: var(--text-2); }
|
|
166
|
+
.menu {
|
|
167
|
+
position: absolute; right: 0; top: calc(100% + 4px); z-index: 50; min-width: 196px;
|
|
168
|
+
background: var(--surface-2); border: 1px solid #2E2E38; border-radius: 10px; padding: 6px;
|
|
169
|
+
box-shadow: 0 16px 40px rgba(0,0,0,.55); display: none;
|
|
170
|
+
}
|
|
171
|
+
.menu.open { display: block; }
|
|
172
|
+
.menu-up { top: auto; bottom: calc(100% + 4px); }
|
|
173
|
+
.menu-item {
|
|
174
|
+
display: flex; align-items: center; gap: 10px; width: 100%; height: 32px; padding: 0 8px;
|
|
175
|
+
border-radius: 6px; border: 0; background: none; color: var(--text);
|
|
176
|
+
font-family: inherit; font-size: 13px; text-align: left; cursor: pointer;
|
|
177
|
+
}
|
|
178
|
+
.menu-item:hover { background: #26262E; }
|
|
179
|
+
.menu-item svg { color: var(--text-2); flex: 0 0 16px; }
|
|
180
|
+
.menu-item.danger, .menu-item.danger svg { color: var(--red); }
|
|
181
|
+
.menu-item.active svg { color: var(--accent-soft); }
|
|
182
|
+
.menu-sep { height: 1px; background: #2E2E38; margin: 5px -6px; }
|
|
183
|
+
|
|
184
|
+
/* Bandeaux */
|
|
185
|
+
.banner {
|
|
186
|
+
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
|
187
|
+
border-radius: 10px; font-size: 13px; margin-bottom: 24px;
|
|
188
|
+
}
|
|
189
|
+
.banner svg { flex: 0 0 16px; }
|
|
190
|
+
.banner-warn { background: rgba(245,158,11,.08); border: 1px solid rgba(245,158,11,.25); color: #FBBF24; }
|
|
191
|
+
.banner-error { background: rgba(239,68,68,.08); border: 1px solid rgba(239,68,68,.25); color: var(--red); }
|
|
192
|
+
.banner-action {
|
|
193
|
+
margin-left: auto; background: none; border: 0; color: inherit;
|
|
194
|
+
font-family: inherit; font-size: 13px; font-weight: 500; cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
.banner-action:hover { text-decoration: underline; }
|
|
197
|
+
|
|
198
|
+
/* Modale */
|
|
199
|
+
.overlay {
|
|
200
|
+
position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 100;
|
|
201
|
+
display: none; align-items: center; justify-content: center; padding: 24px;
|
|
202
|
+
}
|
|
203
|
+
.overlay.active { display: flex; }
|
|
204
|
+
.modal {
|
|
205
|
+
background: var(--surface-2); border: 1px solid var(--border); border-radius: 14px;
|
|
206
|
+
padding: 24px; width: 100%; max-width: 400px; max-height: 90vh; overflow-y: auto;
|
|
207
|
+
}
|
|
208
|
+
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
209
|
+
.modal-head h2 { font-size: 17px; font-weight: 600; }
|
|
210
|
+
|
|
211
|
+
/* Divers */
|
|
212
|
+
.empty {
|
|
213
|
+
border: 1px dashed var(--border); border-radius: 12px; padding: 48px 24px;
|
|
214
|
+
text-align: center; color: var(--text-3); font-size: 14px;
|
|
215
|
+
}
|
|
216
|
+
.empty strong { display: block; color: var(--text-2); font-size: 15px; margin-bottom: 6px; font-weight: 600; }
|
|
217
|
+
.spinner {
|
|
218
|
+
display: inline-block; width: 12px; height: 12px; border-radius: 50%;
|
|
219
|
+
border: 2px solid currentColor; border-top-color: transparent; animation: spin .7s linear infinite;
|
|
220
|
+
}
|
|
221
|
+
.spinning { animation: spin .7s linear infinite; }
|
|
222
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
223
|
+
.hidden { display: none !important; }
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
/** Mise en page à panneau latéral fixe. */
|
|
227
|
+
const SHELL_CSS = `
|
|
228
|
+
.app { display: flex; min-height: 100vh; }
|
|
229
|
+
.sidebar {
|
|
230
|
+
width: 240px; flex: 0 0 240px; background: var(--sidebar);
|
|
231
|
+
border-right: 1px solid var(--border); padding: 20px 0;
|
|
232
|
+
display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
|
|
233
|
+
}
|
|
234
|
+
.brand { display: flex; align-items: center; gap: 12px; padding: 0 20px 26px; }
|
|
235
|
+
.brand-logo {
|
|
236
|
+
width: 32px; height: 32px; border-radius: 9px; background: var(--accent);
|
|
237
|
+
display: grid; place-items: center; color: #fff; flex-shrink: 0;
|
|
238
|
+
}
|
|
239
|
+
.brand-text { min-width: 0; }
|
|
240
|
+
.brand-name { font-size: 14px; font-weight: 600; line-height: 1.3; }
|
|
241
|
+
.brand-host {
|
|
242
|
+
font-size: 11px; color: var(--text-3); font-family: var(--mono);
|
|
243
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
.nav-label {
|
|
246
|
+
font-size: 10px; font-weight: 600; letter-spacing: .09em;
|
|
247
|
+
color: var(--text-4); padding: 0 20px; margin-bottom: 8px;
|
|
248
|
+
}
|
|
249
|
+
.nav { display: flex; flex-direction: column; gap: 2px; padding: 0 12px; margin-bottom: 26px; }
|
|
250
|
+
.nav-item {
|
|
251
|
+
display: flex; align-items: center; gap: 12px; width: 100%; height: 36px; padding: 0 16px;
|
|
252
|
+
border: 0; background: none; border-radius: 8px; color: var(--text-2);
|
|
253
|
+
font-family: inherit; font-size: 13px; text-align: left; cursor: pointer; position: relative;
|
|
254
|
+
}
|
|
255
|
+
.nav-item:hover { background: var(--surface); color: var(--text); }
|
|
256
|
+
.nav-item.active { background: var(--surface-2); color: var(--text); font-weight: 500; }
|
|
257
|
+
.nav-item.active::before {
|
|
258
|
+
content: ''; position: absolute; left: 0; top: 8px; bottom: 8px;
|
|
259
|
+
width: 2px; border-radius: 1px; background: var(--accent);
|
|
260
|
+
}
|
|
261
|
+
.nav-item > svg { flex: 0 0 16px; color: var(--text-3); }
|
|
262
|
+
.nav-item.active > svg { color: #A5B4FC; }
|
|
263
|
+
.nav-item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
264
|
+
.nav-count { font-size: 12px; color: var(--text-4); flex-shrink: 0; }
|
|
265
|
+
.nav-item.active .nav-count { color: var(--text-3); }
|
|
266
|
+
.nav-warn { color: var(--amber); display: none; flex-shrink: 0; }
|
|
267
|
+
.nav-warn.visible { display: block; }
|
|
268
|
+
.nav-item-plain { height: 32px; padding: 0 20px; }
|
|
269
|
+
.nav-item-plain .nav-item-label { font-size: 13px; }
|
|
270
|
+
.nav-item-muted { color: var(--text-3); }
|
|
271
|
+
.sidebar-spacer { flex: 1 1 auto; min-height: 24px; }
|
|
272
|
+
.status-card {
|
|
273
|
+
margin: 0 16px; padding: 14px 16px; border-radius: 10px;
|
|
274
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
275
|
+
}
|
|
276
|
+
.status-line { display: flex; align-items: center; gap: 10px; font-size: 12px; font-weight: 500; }
|
|
277
|
+
.status-detail { font-size: 11px; color: var(--text-3); margin-top: 6px; }
|
|
278
|
+
.main { flex: 1 1 auto; min-width: 0; padding: 24px 32px 64px; }
|
|
279
|
+
.main-inner { max-width: 1160px; margin: 0 auto; }
|
|
280
|
+
|
|
281
|
+
@media (max-width: 900px) {
|
|
282
|
+
.app { flex-direction: column; }
|
|
283
|
+
.sidebar {
|
|
284
|
+
width: auto; flex: none; height: auto; position: static;
|
|
285
|
+
border-right: 0; border-bottom: 1px solid var(--border); padding: 16px 0;
|
|
286
|
+
}
|
|
287
|
+
.sidebar-spacer { display: none; }
|
|
288
|
+
.nav { flex-direction: row; flex-wrap: wrap; margin-bottom: 16px; }
|
|
289
|
+
.nav-item { width: auto; }
|
|
290
|
+
.nav-item.active::before { display: none; }
|
|
291
|
+
.status-card { display: none; }
|
|
292
|
+
.main { padding: 20px 16px 48px; }
|
|
293
|
+
.page-head { flex-wrap: wrap; }
|
|
294
|
+
.page-actions { margin-left: 0; width: 100%; }
|
|
295
|
+
}
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Panneau latéral commun.
|
|
300
|
+
* @param {Object} o
|
|
301
|
+
* @param {string} o.proxyName
|
|
302
|
+
* @param {string} [o.host] - libellé sous le nom du produit
|
|
303
|
+
* @param {'services'|'agent'|'meta'} o.active - page courante
|
|
304
|
+
* @param {number} [o.serviceCount]
|
|
305
|
+
* @param {boolean} [o.hasCleanUp]
|
|
306
|
+
* @param {boolean} [o.hasAuth]
|
|
307
|
+
* @param {string} [o.sectionLabel] - titre de la section contextuelle
|
|
308
|
+
* @param {string} [o.sectionHtml] - contenu de la section contextuelle
|
|
309
|
+
* @param {string} [o.statusDetail] - deuxième ligne de la carte de statut
|
|
310
|
+
*/
|
|
311
|
+
function renderSidebar(o) {
|
|
312
|
+
const count = typeof o.serviceCount === 'number'
|
|
313
|
+
? `<span class="nav-count">${o.serviceCount}</span>`
|
|
314
|
+
: '';
|
|
315
|
+
|
|
316
|
+
return `<aside class="sidebar">
|
|
317
|
+
<div class="brand">
|
|
318
|
+
<span class="brand-logo">${icon('network', 16)}</span>
|
|
319
|
+
<span class="brand-text">
|
|
320
|
+
<span class="brand-name">${escapeHtml(o.proxyName)}</span>
|
|
321
|
+
<span class="brand-host">${escapeHtml(o.host || '')}</span>
|
|
322
|
+
</span>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div class="nav-label">NAVIGATION</div>
|
|
326
|
+
<nav class="nav">
|
|
327
|
+
<a class="nav-item${o.active === 'services' ? ' active' : ''}" href="/">
|
|
328
|
+
${icon('layers')}<span class="nav-item-label">Services</span>${count}
|
|
329
|
+
</a>
|
|
330
|
+
<a class="nav-item${o.active === 'agent' ? ' active' : ''}" href="/proxy/terminal" target="_blank" rel="noopener noreferrer">
|
|
331
|
+
${icon('terminal')}<span class="nav-item-label">Agent</span>
|
|
332
|
+
</a>
|
|
333
|
+
<a class="nav-item${o.active === 'meta' ? ' active' : ''}" href="/proxy/meta-configs-ui">
|
|
334
|
+
${icon('sliders')}<span class="nav-item-label">Meta configs</span>
|
|
335
|
+
<span class="nav-warn" id="navSyncWarn" title="Configurations désynchronisées">${icon('alert', 14)}</span>
|
|
336
|
+
</a>
|
|
337
|
+
${o.hasCleanUp ? `<button class="nav-item" id="cleanupBtn" onclick="runCleanUp()">
|
|
338
|
+
${icon('trash')}<span class="nav-item-label">Nettoyer les process</span>
|
|
339
|
+
</button>` : ''}
|
|
340
|
+
</nav>
|
|
341
|
+
|
|
342
|
+
${o.sectionLabel ? `<div class="nav-label">${escapeHtml(o.sectionLabel)}</div>
|
|
343
|
+
<nav class="nav">${o.sectionHtml || ''}</nav>` : ''}
|
|
344
|
+
|
|
345
|
+
<div class="sidebar-spacer"></div>
|
|
346
|
+
|
|
347
|
+
${o.hasAuth ? `<nav class="nav" style="margin-bottom:16px;">
|
|
348
|
+
<button class="nav-item" onclick="logout()">
|
|
349
|
+
${icon('logout')}<span class="nav-item-label">Déconnexion</span>
|
|
350
|
+
</button>
|
|
351
|
+
</nav>` : ''}
|
|
352
|
+
|
|
353
|
+
<div class="status-card">
|
|
354
|
+
<div class="status-line"><span class="dot up"></span><span>Proxy en ligne</span></div>
|
|
355
|
+
<div class="status-detail">${escapeHtml(o.statusDetail || '')}</div>
|
|
356
|
+
</div>
|
|
357
|
+
</aside>`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Scripts partagés : déconnexion, nettoyage, menus contextuels, badge de désync. */
|
|
361
|
+
const SHELL_JS = `
|
|
362
|
+
function logout() {
|
|
363
|
+
const form = document.createElement('form');
|
|
364
|
+
form.method = 'POST';
|
|
365
|
+
form.action = '/proxy/logout';
|
|
366
|
+
document.body.appendChild(form);
|
|
367
|
+
form.submit();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function runCleanUp() {
|
|
371
|
+
if (!confirm('Nettoyer tous les processus ?')) return;
|
|
372
|
+
const btn = document.getElementById('cleanupBtn');
|
|
373
|
+
const label = btn.querySelector('.nav-item-label');
|
|
374
|
+
const original = label.textContent;
|
|
375
|
+
btn.disabled = true;
|
|
376
|
+
label.textContent = 'Nettoyage…';
|
|
377
|
+
try {
|
|
378
|
+
await fetch('/proxy/cleanup', { method: 'POST' });
|
|
379
|
+
label.textContent = 'Nettoyé';
|
|
380
|
+
} catch (_) {
|
|
381
|
+
label.textContent = 'Échec du nettoyage';
|
|
382
|
+
} finally {
|
|
383
|
+
setTimeout(() => { label.textContent = original; btn.disabled = false; }, 3000);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* Un seul menu ouvert à la fois ; clic extérieur et Échap le referment. */
|
|
388
|
+
function toggleMenu(btn) {
|
|
389
|
+
const menu = btn.nextElementSibling;
|
|
390
|
+
const wasOpen = menu.classList.contains('open');
|
|
391
|
+
closeAllMenus();
|
|
392
|
+
if (wasOpen) return;
|
|
393
|
+
menu.classList.remove('menu-up');
|
|
394
|
+
menu.classList.add('open');
|
|
395
|
+
// Bascule au-dessus du bouton quand le bas de la fenêtre est trop proche.
|
|
396
|
+
if (menu.getBoundingClientRect().bottom > window.innerHeight - 8) {
|
|
397
|
+
menu.classList.add('menu-up');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function closeAllMenus() {
|
|
402
|
+
document.querySelectorAll('.menu.open').forEach(m => m.classList.remove('open'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
document.addEventListener('click', (e) => {
|
|
406
|
+
if (!e.target.closest('.menu-wrap')) closeAllMenus();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
document.addEventListener('keydown', (e) => {
|
|
410
|
+
if (e.key === 'Escape') closeAllMenus();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
/** Signale la désynchronisation des méta-configs dans le panneau. */
|
|
414
|
+
async function refreshSyncBadge() {
|
|
415
|
+
const el = document.getElementById('navSyncWarn');
|
|
416
|
+
if (!el) return;
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch('/proxy/meta-configs/active/sync');
|
|
419
|
+
const data = await res.json();
|
|
420
|
+
el.classList.toggle('visible', data.synced === false);
|
|
421
|
+
} catch (_) {}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function esc(str) {
|
|
425
|
+
const d = document.createElement('div');
|
|
426
|
+
d.textContent = String(str == null ? '' : str);
|
|
427
|
+
return d.innerHTML;
|
|
428
|
+
}
|
|
429
|
+
`;
|
|
430
|
+
|
|
431
|
+
module.exports = { BASE_CSS, SHELL_CSS, SHELL_JS, renderSidebar, icon };
|