somratpro Claude Sonnet 4.6 commited on
Commit
b25ba68
Β·
1 Parent(s): 4c78093

feat: add ENV Builder at /env-builder

Browse files

Port env-builder from HuggingClaw, adapted for HuggingMes/Hermes vars:
- 8 sections: Core, Backup, Telegram, Terminal, Providers, Cloudflare, Advanced, Custom
- Model catalog with provider/model prefix format (gemini/, anthropic/, openrouter/, etc.)
- Import/export as HUGGINGMES_ENV_BUNDLE base64 or plain .env
- No auth required (useful before GATEWAY_TOKEN is set)
- Dashboard button: "ENV Builder β†’" beside Open Hermes Agent and Open Terminal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. Dockerfile +2 -0
  2. env-builder.html +777 -0
  3. env-builder.js +796 -0
  4. health-server.js +25 -0
Dockerfile CHANGED
@@ -37,6 +37,8 @@ COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
37
  COPY --chown=hermes:hermes hermes-sync.py /opt/huggingmes/hermes-sync.py
38
  COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/huggingmes/cloudflare-proxy-setup.py
39
  COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/huggingmes/cloudflare-keepalive-setup.py
 
 
40
 
41
  RUN chmod +x \
42
  /opt/huggingmes/start.sh \
 
37
  COPY --chown=hermes:hermes hermes-sync.py /opt/huggingmes/hermes-sync.py
38
  COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/huggingmes/cloudflare-proxy-setup.py
39
  COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/huggingmes/cloudflare-keepalive-setup.py
40
+ COPY --chown=hermes:hermes env-builder.html /opt/huggingmes/env-builder.html
41
+ COPY --chown=hermes:hermes env-builder.js /opt/huggingmes/env-builder.js
42
 
43
  RUN chmod +x \
44
  /opt/huggingmes/start.sh \
env-builder.html ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HuggingMes Β· ENV Builder</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10
+
11
+ <style>
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+
14
+ :root {
15
+ --bg: #0b0c0f;
16
+ --bg2: #111318;
17
+ --bg3: #181c23;
18
+ --bg4: #1e2330;
19
+ --border: #252b38;
20
+ --border2: #2e3648;
21
+ --amber: #f5a623;
22
+ --amber2: #ffbe55;
23
+ --amber-dim: rgba(245,166,35,.12);
24
+ --amber-glow:rgba(245,166,35,.22);
25
+ --green: #3dd68c;
26
+ --red: #f05f5f;
27
+ --blue: #5b8af5;
28
+ --text: #e4e8f0;
29
+ --text2: #8d97ad;
30
+ --text3: #535f76;
31
+ --mono: 'JetBrains Mono', monospace;
32
+ --sans: 'Syne', sans-serif;
33
+ --r: 8px;
34
+ --r2: 12px;
35
+ --sidebar-w: 220px;
36
+ --panel-w: 340px;
37
+ }
38
+
39
+ html { scroll-behavior: smooth; }
40
+
41
+ body {
42
+ font-family: var(--sans);
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ min-height: 100vh;
46
+ display: flex;
47
+ flex-direction: column;
48
+ overflow-x: hidden;
49
+ }
50
+
51
+ .topbar {
52
+ position: sticky;
53
+ top: 0;
54
+ z-index: 100;
55
+ height: 52px;
56
+ background: rgba(11,12,15,.9);
57
+ backdrop-filter: blur(14px);
58
+ border-bottom: 1px solid var(--border);
59
+ display: flex;
60
+ align-items: center;
61
+ padding: 0 20px;
62
+ gap: 16px;
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ .topbar-logo {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 10px;
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ .topbar-logo .logo-emoji { font-size: 24px; line-height: 1; }
74
+
75
+ .topbar-wordmark {
76
+ font-weight: 800;
77
+ font-size: 14px;
78
+ letter-spacing: -.2px;
79
+ color: var(--text);
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .topbar-wordmark em {
84
+ color: var(--amber);
85
+ font-style: normal;
86
+ }
87
+
88
+ .topbar-divider {
89
+ width: 1px;
90
+ height: 22px;
91
+ background: var(--border2);
92
+ }
93
+
94
+ .topbar-title {
95
+ font-size: 12px;
96
+ font-weight: 600;
97
+ color: var(--text2);
98
+ letter-spacing: .5px;
99
+ text-transform: uppercase;
100
+ }
101
+
102
+ .topbar-spacer { flex: 1; }
103
+
104
+ .topbar-pill {
105
+ font-family: var(--mono);
106
+ font-size: 10px;
107
+ color: var(--amber);
108
+ background: var(--amber-dim);
109
+ border: 1px solid var(--amber-glow);
110
+ border-radius: 20px;
111
+ padding: 3px 10px;
112
+ letter-spacing: .5px;
113
+ }
114
+
115
+ .layout {
116
+ display: flex;
117
+ flex: 1;
118
+ min-height: 0;
119
+ height: calc(100vh - 52px);
120
+ }
121
+
122
+ .sidebar-wrap {
123
+ width: var(--sidebar-w);
124
+ flex-shrink: 0;
125
+ border-right: 1px solid var(--border);
126
+ background: var(--bg2);
127
+ display: flex;
128
+ flex-direction: column;
129
+ overflow: hidden;
130
+ }
131
+
132
+ .sidebar-scroll {
133
+ flex: 1;
134
+ overflow-y: auto;
135
+ padding: 14px 10px;
136
+ }
137
+
138
+ .sidebar-scroll::-webkit-scrollbar { width: 4px; }
139
+ .sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
140
+ .sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
141
+
142
+ .sb-label {
143
+ font-size: 9px;
144
+ font-weight: 700;
145
+ text-transform: uppercase;
146
+ letter-spacing: 1.2px;
147
+ color: var(--text3);
148
+ padding: 0 8px 10px;
149
+ }
150
+
151
+ .nav-btn {
152
+ width: 100%;
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ padding: 8px 10px;
157
+ border: none;
158
+ background: transparent;
159
+ cursor: pointer;
160
+ border-radius: var(--r);
161
+ text-align: left;
162
+ color: var(--text2);
163
+ font-family: var(--sans);
164
+ font-size: 12.5px;
165
+ font-weight: 500;
166
+ transition: background .15s, color .15s;
167
+ margin-bottom: 2px;
168
+ }
169
+
170
+ .nav-btn:hover { background: var(--bg3); color: var(--text); }
171
+ .nav-btn.active {
172
+ background: var(--amber-dim);
173
+ color: var(--amber);
174
+ border: 1px solid var(--amber-glow);
175
+ }
176
+
177
+ .nav-icon { font-size: 13px; flex-shrink: 0; }
178
+ .nav-label { flex: 1; }
179
+ .nav-count {
180
+ font-family: var(--mono);
181
+ font-size: 10px;
182
+ font-weight: 600;
183
+ color: var(--text3);
184
+ background: var(--bg3);
185
+ border-radius: 10px;
186
+ padding: 1px 6px;
187
+ min-width: 20px;
188
+ text-align: center;
189
+ transition: background .2s, color .2s;
190
+ }
191
+ .nav-btn.active .nav-count {
192
+ background: var(--amber-glow);
193
+ color: var(--amber2);
194
+ }
195
+
196
+ .main {
197
+ flex: 1;
198
+ display: flex;
199
+ flex-direction: column;
200
+ min-width: 0;
201
+ overflow: hidden;
202
+ }
203
+
204
+ .toolbar {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 10px;
208
+ padding: 12px 20px;
209
+ border-bottom: 1px solid var(--border);
210
+ background: var(--bg2);
211
+ flex-shrink: 0;
212
+ flex-wrap: wrap;
213
+ }
214
+
215
+ .search-wrap {
216
+ position: relative;
217
+ flex: 1;
218
+ min-width: 160px;
219
+ max-width: 340px;
220
+ }
221
+
222
+ .search-icon {
223
+ position: absolute;
224
+ left: 10px;
225
+ top: 50%;
226
+ transform: translateY(-50%);
227
+ color: var(--text3);
228
+ pointer-events: none;
229
+ font-size: 12px;
230
+ }
231
+
232
+ #search {
233
+ width: 100%;
234
+ background: var(--bg3);
235
+ border: 1px solid var(--border2);
236
+ border-radius: var(--r);
237
+ padding: 7px 10px 7px 30px;
238
+ font-family: var(--mono);
239
+ font-size: 12px;
240
+ color: var(--text);
241
+ outline: none;
242
+ transition: border-color .15s;
243
+ }
244
+
245
+ #search:focus { border-color: var(--amber); }
246
+ #search::placeholder { color: var(--text3); }
247
+
248
+ .tb-sep {
249
+ width: 1px;
250
+ height: 24px;
251
+ background: var(--border2);
252
+ }
253
+
254
+ .btn {
255
+ display: inline-flex;
256
+ align-items: center;
257
+ gap: 5px;
258
+ padding: 6px 13px;
259
+ border-radius: var(--r);
260
+ border: 1px solid var(--border2);
261
+ background: var(--bg3);
262
+ color: var(--text2);
263
+ font-family: var(--sans);
264
+ font-size: 11.5px;
265
+ font-weight: 600;
266
+ cursor: pointer;
267
+ transition: all .15s;
268
+ white-space: nowrap;
269
+ }
270
+
271
+ .btn:hover { background: var(--bg4); color: var(--text); border-color: var(--border2); }
272
+
273
+ .btn-amber {
274
+ background: var(--amber);
275
+ color: #0b0c0f;
276
+ border-color: var(--amber);
277
+ }
278
+ .btn-amber:hover { background: var(--amber2); border-color: var(--amber2); }
279
+
280
+ .btn-ghost {
281
+ background: transparent;
282
+ border-color: transparent;
283
+ color: var(--text3);
284
+ }
285
+ .btn-ghost:hover { background: var(--bg3); color: var(--text2); border-color: var(--border2); }
286
+
287
+ .content-wrap {
288
+ flex: 1;
289
+ display: flex;
290
+ min-height: 0;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .sections-scroll {
295
+ flex: 1;
296
+ overflow-y: auto;
297
+ padding: 16px 20px 80px;
298
+ min-width: 0;
299
+ }
300
+
301
+ .sections-scroll::-webkit-scrollbar { width: 5px; }
302
+ .sections-scroll::-webkit-scrollbar-track { background: transparent; }
303
+ .sections-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
304
+
305
+ .sec { margin-bottom: 28px; }
306
+ .sec.sec-hidden { display: none !important; }
307
+
308
+ .sec-header {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 8px;
312
+ margin-bottom: 12px;
313
+ }
314
+
315
+ .sec-icon { font-size: 14px; }
316
+ .sec-title {
317
+ font-size: 11px;
318
+ font-weight: 700;
319
+ text-transform: uppercase;
320
+ letter-spacing: 1.2px;
321
+ color: var(--text3);
322
+ }
323
+
324
+ .sec-line {
325
+ flex: 1;
326
+ height: 1px;
327
+ background: var(--border);
328
+ }
329
+
330
+ .cards {
331
+ display: grid;
332
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
333
+ gap: 10px;
334
+ }
335
+
336
+ .env-card {
337
+ background: var(--bg2);
338
+ border: 1px solid var(--border);
339
+ border-radius: var(--r2);
340
+ padding: 12px;
341
+ transition: border-color .2s, background .2s;
342
+ }
343
+
344
+ .env-card:hover { border-color: var(--border2); }
345
+ .env-card.hidden { display: none; }
346
+
347
+ .env-card.selected {
348
+ border-color: var(--amber-glow);
349
+ background: linear-gradient(135deg, var(--bg2) 80%, rgba(245,166,35,.04));
350
+ }
351
+
352
+ .card-top {
353
+ display: flex;
354
+ align-items: flex-start;
355
+ gap: 9px;
356
+ margin-bottom: 9px;
357
+ }
358
+
359
+ .card-check {
360
+ width: 15px;
361
+ height: 15px;
362
+ accent-color: var(--amber);
363
+ flex-shrink: 0;
364
+ margin-top: 2px;
365
+ cursor: pointer;
366
+ }
367
+
368
+ .card-info { flex: 1; min-width: 0; }
369
+
370
+ .card-key {
371
+ font-family: var(--mono);
372
+ font-size: 11.5px;
373
+ font-weight: 600;
374
+ color: var(--text);
375
+ letter-spacing: .3px;
376
+ white-space: nowrap;
377
+ overflow: hidden;
378
+ text-overflow: ellipsis;
379
+ }
380
+
381
+ .card-lbl {
382
+ font-size: 11px;
383
+ color: var(--text3);
384
+ margin-top: 2px;
385
+ line-height: 1.35;
386
+ }
387
+
388
+ .badge {
389
+ flex-shrink: 0;
390
+ font-family: var(--mono);
391
+ font-size: 9px;
392
+ font-weight: 700;
393
+ text-transform: uppercase;
394
+ letter-spacing: .6px;
395
+ padding: 2px 7px;
396
+ border-radius: 20px;
397
+ }
398
+
399
+ .badge-s {
400
+ background: rgba(240,95,95,.12);
401
+ color: var(--red);
402
+ border: 1px solid rgba(240,95,95,.25);
403
+ }
404
+
405
+ .badge-f {
406
+ background: rgba(61,214,140,.1);
407
+ color: var(--green);
408
+ border: 1px solid rgba(61,214,140,.2);
409
+ }
410
+
411
+ .card-input { position: relative; }
412
+
413
+ .card-input input[type="text"],
414
+ .card-input input[type="password"],
415
+ .card-input input[type="number"],
416
+ .card-input textarea,
417
+ .card-input select {
418
+ width: 100%;
419
+ background: var(--bg3);
420
+ border: 1px solid var(--border);
421
+ border-radius: var(--r);
422
+ padding: 7px 10px;
423
+ font-family: var(--mono);
424
+ font-size: 11.5px;
425
+ color: var(--text);
426
+ outline: none;
427
+ transition: border-color .15s;
428
+ resize: vertical;
429
+ }
430
+
431
+ .card-input input[type="text"]:focus,
432
+ .card-input input[type="password"]:focus,
433
+ .card-input input[type="number"]:focus,
434
+ .card-input textarea:focus,
435
+ .card-input select:focus {
436
+ border-color: var(--amber);
437
+ }
438
+
439
+ .card-input textarea { min-height: 64px; }
440
+ .card-input select {
441
+ cursor: pointer;
442
+ appearance: none;
443
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238d97ad' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
444
+ background-repeat: no-repeat;
445
+ background-position: right 10px center;
446
+ padding-right: 28px;
447
+ }
448
+
449
+ .card-input optgroup { color: var(--text2); font-weight: 600; }
450
+ .card-input option { color: var(--text); background: var(--bg3); }
451
+
452
+ .toggle-shell { display: flex; align-items: center; gap: 8px; }
453
+ .tog {
454
+ padding: 5px 14px;
455
+ border-radius: 20px;
456
+ border: 1px solid var(--border2);
457
+ background: var(--bg3);
458
+ color: var(--text3);
459
+ font-family: var(--mono);
460
+ font-size: 11px;
461
+ font-weight: 700;
462
+ cursor: pointer;
463
+ transition: all .18s;
464
+ letter-spacing: .5px;
465
+ }
466
+ .tog.on {
467
+ background: rgba(61,214,140,.15);
468
+ border-color: rgba(61,214,140,.4);
469
+ color: var(--green);
470
+ }
471
+
472
+ .picker-shell { display: flex; flex-direction: column; gap: 6px; }
473
+ .picker-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
474
+
475
+ .picker-select {
476
+ flex: 1;
477
+ min-width: 0;
478
+ padding: 6px 28px 6px 8px !important;
479
+ font-size: 11px !important;
480
+ }
481
+
482
+ .mini-btn {
483
+ padding: 5px 9px;
484
+ border-radius: var(--r);
485
+ border: 1px solid var(--border2);
486
+ background: var(--bg3);
487
+ color: var(--text2);
488
+ font-family: var(--mono);
489
+ font-size: 10px;
490
+ font-weight: 600;
491
+ cursor: pointer;
492
+ transition: all .15s;
493
+ white-space: nowrap;
494
+ }
495
+ .mini-btn:hover { background: var(--bg4); color: var(--text); }
496
+
497
+ .right-panel {
498
+ width: var(--panel-w);
499
+ flex-shrink: 0;
500
+ border-left: 1px solid var(--border);
501
+ background: var(--bg2);
502
+ display: flex;
503
+ flex-direction: column;
504
+ overflow: hidden;
505
+ }
506
+
507
+ .panel-scroll {
508
+ flex: 1;
509
+ overflow-y: auto;
510
+ padding: 16px;
511
+ display: flex;
512
+ flex-direction: column;
513
+ gap: 16px;
514
+ }
515
+
516
+ .panel-scroll::-webkit-scrollbar { width: 4px; }
517
+ .panel-scroll::-webkit-scrollbar-track { background: transparent; }
518
+ .panel-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
519
+
520
+ .pblock {
521
+ background: var(--bg3);
522
+ border: 1px solid var(--border);
523
+ border-radius: var(--r2);
524
+ overflow: hidden;
525
+ }
526
+
527
+ .pblock-head {
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: space-between;
531
+ padding: 10px 14px;
532
+ border-bottom: 1px solid var(--border);
533
+ }
534
+
535
+ .pblock-title {
536
+ font-size: 10.5px;
537
+ font-weight: 700;
538
+ text-transform: uppercase;
539
+ letter-spacing: 1px;
540
+ color: var(--text3);
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 6px;
544
+ }
545
+
546
+ .pblock-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
547
+
548
+ .pblock-body textarea,
549
+ .pblock-body input[type="text"] {
550
+ width: 100%;
551
+ background: var(--bg);
552
+ border: 1px solid var(--border);
553
+ border-radius: var(--r);
554
+ padding: 8px 10px;
555
+ font-family: var(--mono);
556
+ font-size: 10.5px;
557
+ color: var(--text2);
558
+ outline: none;
559
+ resize: vertical;
560
+ transition: border-color .15s;
561
+ }
562
+
563
+ .pblock-body textarea:focus,
564
+ .pblock-body input[type="text"]:focus {
565
+ border-color: var(--amber);
566
+ color: var(--text);
567
+ }
568
+
569
+ #importText { min-height: 80px; }
570
+ #bundleOut { min-height: 60px; color: var(--amber2); }
571
+ #envLineOut { font-size: 10px; }
572
+
573
+ .row-btns { display: flex; gap: 6px; flex-wrap: wrap; }
574
+
575
+ #summary {
576
+ font-size: 11.5px;
577
+ color: var(--text2);
578
+ line-height: 1.6;
579
+ }
580
+
581
+ #summary strong {
582
+ font-size: 15px;
583
+ color: var(--amber);
584
+ font-family: var(--mono);
585
+ }
586
+
587
+ .sum-keys {
588
+ margin-top: 8px;
589
+ display: flex;
590
+ flex-wrap: wrap;
591
+ gap: 4px;
592
+ }
593
+
594
+ .sum-key {
595
+ font-family: var(--mono);
596
+ font-size: 9.5px;
597
+ color: var(--text2);
598
+ background: var(--bg4);
599
+ border: 1px solid var(--border2);
600
+ border-radius: 4px;
601
+ padding: 2px 6px;
602
+ }
603
+
604
+ #customSec { margin-top: 8px; }
605
+
606
+ .custom-row {
607
+ display: flex;
608
+ gap: 8px;
609
+ align-items: center;
610
+ margin-bottom: 8px;
611
+ }
612
+
613
+ .custom-row input {
614
+ flex: 1;
615
+ background: var(--bg3);
616
+ border: 1px solid var(--border);
617
+ border-radius: var(--r);
618
+ padding: 7px 10px;
619
+ font-family: var(--mono);
620
+ font-size: 11px;
621
+ color: var(--text);
622
+ outline: none;
623
+ transition: border-color .15s;
624
+ min-width: 0;
625
+ }
626
+
627
+ .custom-row input:focus { border-color: var(--amber); }
628
+ .custom-row input:first-child { flex: 0 0 40%; }
629
+
630
+ #toast {
631
+ position: fixed;
632
+ bottom: 24px;
633
+ left: 50%;
634
+ transform: translateX(-50%) translateY(20px);
635
+ background: var(--bg4);
636
+ border: 1px solid var(--border2);
637
+ color: var(--amber);
638
+ font-family: var(--mono);
639
+ font-size: 12px;
640
+ font-weight: 600;
641
+ padding: 9px 20px;
642
+ border-radius: 30px;
643
+ z-index: 9999;
644
+ opacity: 0;
645
+ transition: opacity .2s, transform .2s;
646
+ pointer-events: none;
647
+ box-shadow: 0 8px 32px rgba(0,0,0,.5);
648
+ }
649
+
650
+ #toast.show {
651
+ opacity: 1;
652
+ transform: translateX(-50%) translateY(0);
653
+ }
654
+
655
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
656
+ ::-webkit-scrollbar-track { background: transparent; }
657
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
658
+
659
+ @media (max-width: 900px) {
660
+ :root { --panel-w: 280px; --sidebar-w: 180px; }
661
+ }
662
+
663
+ @media (max-width: 700px) {
664
+ .right-panel { display: none; }
665
+ :root { --sidebar-w: 160px; }
666
+ }
667
+
668
+ @media (max-width: 520px) {
669
+ .sidebar-wrap { display: none; }
670
+ .topbar-divider, .topbar-title { display: none; }
671
+ }
672
+ </style>
673
+ </head>
674
+
675
+ <body>
676
+
677
+ <header class="topbar">
678
+ <div class="topbar-logo">
679
+ <span class="logo-emoji">πŸͺ½</span>
680
+ <span class="topbar-wordmark">Hugging<em>Mes</em></span>
681
+ </div>
682
+ <div class="topbar-divider"></div>
683
+ <span class="topbar-title">ENV Builder</span>
684
+ <div class="topbar-spacer"></div>
685
+ <span class="topbar-pill">v2025</span>
686
+ </header>
687
+
688
+ <div class="layout">
689
+
690
+ <aside class="sidebar-wrap">
691
+ <div class="sidebar-scroll">
692
+ <div id="sidebar"></div>
693
+ </div>
694
+ </aside>
695
+
696
+ <main class="main">
697
+
698
+ <div class="toolbar">
699
+ <div class="search-wrap">
700
+ <span class="search-icon">βŒ•</span>
701
+ <input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
702
+ </div>
703
+
704
+ <div class="tb-sep"></div>
705
+
706
+ <button id="selectCommon" class="btn">β˜… Common</button>
707
+ <button id="selectVisible" class="btn">β˜‘ Visible</button>
708
+ <button id="clearAll" class="btn btn-ghost">βœ• Clear</button>
709
+ </div>
710
+
711
+ <div class="content-wrap">
712
+
713
+ <div class="sections-scroll">
714
+ <div id="sections"></div>
715
+
716
+ <div id="customSec" class="sec" data-section="Custom Env">
717
+ <div class="sec-header">
718
+ <span class="sec-icon">πŸ”§</span>
719
+ <span class="sec-title">Custom Env</span>
720
+ <div class="sec-line"></div>
721
+ </div>
722
+ <div id="customRows"></div>
723
+ <button id="addCustom" class="btn" style="margin-top:6px;">+ Add variable</button>
724
+ </div>
725
+ </div>
726
+
727
+ <aside class="right-panel">
728
+ <div class="panel-scroll">
729
+
730
+ <div class="pblock">
731
+ <div class="pblock-head">
732
+ <span class="pblock-title">πŸ“Š Summary</span>
733
+ </div>
734
+ <div class="pblock-body">
735
+ <div id="summary">No variables selected yet.</div>
736
+ </div>
737
+ </div>
738
+
739
+ <div class="pblock">
740
+ <div class="pblock-head">
741
+ <span class="pblock-title">πŸ“¦ Bundle Output</span>
742
+ </div>
743
+ <div class="pblock-body">
744
+ <textarea id="bundleOut" placeholder="Your encoded bundle will appear here…" readonly spellcheck="false"></textarea>
745
+ <input type="text" id="envLineOut" placeholder="HUGGINGMES_ENV_BUNDLE=…" readonly spellcheck="false">
746
+ <div class="row-btns">
747
+ <button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
748
+ <button id="copyEnvLine" class="btn">⎘ Env Line</button>
749
+ <button id="copyJson" class="btn">⎘ JSON</button>
750
+ <button id="applyBundle" class="btn btn-ghost">β†Ί Apply</button>
751
+ </div>
752
+ </div>
753
+ </div>
754
+
755
+ <div class="pblock">
756
+ <div class="pblock-head">
757
+ <span class="pblock-title">πŸ“₯ Import</span>
758
+ </div>
759
+ <div class="pblock-body">
760
+ <textarea id="importText" placeholder="Paste .env, JSON, or HUGGINGMES_ENV_BUNDLE=… here" spellcheck="false"></textarea>
761
+ <button id="applyImport" class="btn btn-amber" style="width:100%;">↓ Import & Apply</button>
762
+ </div>
763
+ </div>
764
+
765
+ </div>
766
+ </aside>
767
+
768
+ </div>
769
+ </main>
770
+ </div>
771
+
772
+ <div id="toast">Copied βœ“</div>
773
+
774
+ <script src="env-builder.js"></script>
775
+
776
+ </body>
777
+ </html>
env-builder.js ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ── Model Catalogs ──
2
+ const MODEL_CATALOGS = {
3
+ "LLM_MODEL": {
4
+ "Anthropic": [
5
+ "anthropic/claude-opus-4-7",
6
+ "anthropic/claude-opus-4-6",
7
+ "anthropic/claude-sonnet-4-6",
8
+ "anthropic/claude-sonnet-4-5",
9
+ "anthropic/claude-haiku-4-5",
10
+ "anthropic/claude-haiku-3-5"
11
+ ],
12
+ "Gemini": [
13
+ "gemini/gemini-2.5-pro-preview-06-05",
14
+ "gemini/gemini-2.5-flash-preview-05-20",
15
+ "gemini/gemini-2.5-flash",
16
+ "gemini/gemini-2.0-flash",
17
+ "gemini/gemini-1.5-pro",
18
+ "gemini/gemini-1.5-flash",
19
+ "google/gemini-2.5-flash",
20
+ "google/gemini-2.0-flash"
21
+ ],
22
+ "OpenAI": [
23
+ "openai/gpt-4.1",
24
+ "openai/gpt-4.1-mini",
25
+ "openai/gpt-4o",
26
+ "openai/gpt-4o-mini",
27
+ "openai/o3",
28
+ "openai/o4-mini",
29
+ "openai/o3-mini"
30
+ ],
31
+ "OpenRouter": [
32
+ "openrouter/anthropic/claude-opus-4-7",
33
+ "openrouter/anthropic/claude-sonnet-4-6",
34
+ "openrouter/anthropic/claude-haiku-4-5",
35
+ "openrouter/openai/gpt-4o",
36
+ "openrouter/openai/o3",
37
+ "openrouter/google/gemini-2.5-flash",
38
+ "openrouter/google/gemini-2.5-pro",
39
+ "openrouter/meta-llama/llama-4-maverick",
40
+ "openrouter/deepseek/deepseek-r1",
41
+ "openrouter/deepseek/deepseek-chat-v3-5",
42
+ "openrouter/mistralai/mistral-large"
43
+ ],
44
+ "DeepSeek": [
45
+ "deepseek/deepseek-chat",
46
+ "deepseek/deepseek-reasoner"
47
+ ],
48
+ "xAI": [
49
+ "xai/grok-3",
50
+ "xai/grok-3-mini",
51
+ "xai/grok-2"
52
+ ],
53
+ "HuggingFace": [
54
+ "huggingface/meta-llama/Llama-3.3-70B-Instruct",
55
+ "huggingface/meta-llama/Llama-3.1-70B-Instruct",
56
+ "huggingface/Qwen/Qwen2.5-72B-Instruct",
57
+ "huggingface/mistralai/Mistral-7B-Instruct-v0.3",
58
+ "huggingface/google/gemma-2-27b-it"
59
+ ],
60
+ "Moonshot / Kimi": [
61
+ "moonshot/moonshot-v1-128k",
62
+ "kimi-coding/kimi-k2-0711-preview",
63
+ "kimi-coding-cn/kimi-k2-0711-preview"
64
+ ],
65
+ "Alibaba": [
66
+ "alibaba/qwen-max",
67
+ "alibaba/qwen-plus",
68
+ "alibaba/qwen-turbo"
69
+ ],
70
+ "Minimax": [
71
+ "minimax/minimax-01",
72
+ "minimax-cn/minimax-01"
73
+ ],
74
+ "NVIDIA": [
75
+ "nvidia/meta/llama-3.1-70b-instruct",
76
+ "nvidia/meta/llama-3.3-70b-instruct"
77
+ ],
78
+ "GLM / ZAI": [
79
+ "zai/glm-4-plus",
80
+ "glm/chatglm-turbo"
81
+ ],
82
+ "Vercel AI Gateway": [
83
+ "vercel-ai-gateway/anthropic/claude-sonnet-4-6",
84
+ "vercel-ai-gateway/openai/gpt-4o"
85
+ ],
86
+ "Custom / OpenAI-compatible": [
87
+ "custom"
88
+ ]
89
+ }
90
+ };
91
+
92
+ // ── Icons per group ──
93
+ const ICONS = {
94
+ "All": "🌐",
95
+ "Core": "⚑",
96
+ "Backup": "πŸ’Ύ",
97
+ "Telegram": "πŸ“±",
98
+ "Terminal": "πŸ’»",
99
+ "Providers": "πŸ”‘",
100
+ "Cloudflare":"☁️",
101
+ "Advanced": "βš™οΈ",
102
+ "Custom Env":"πŸ”§"
103
+ };
104
+
105
+ // ── Field definitions ──
106
+ const FIELDS = [
107
+ // ── Core ──
108
+ {
109
+ "g": "Core", "icon": "⚑",
110
+ "k": "GATEWAY_TOKEN",
111
+ "lbl": "Gateway token β€” protects the Hermes web UI",
112
+ "type": "password", "secret": 1, "common": 1
113
+ },
114
+ {
115
+ "g": "Core", "icon": "⚑",
116
+ "k": "LLM_MODEL",
117
+ "lbl": "Default model (provider/model-name format)",
118
+ "type": "model", "options_key": "LLM_MODEL",
119
+ "ph": "gemini/gemini-2.5-flash", "common": 1
120
+ },
121
+ {
122
+ "g": "Core", "icon": "⚑",
123
+ "k": "LLM_API_KEY",
124
+ "lbl": "API key for the chosen provider",
125
+ "type": "password", "secret": 1, "common": 1
126
+ },
127
+
128
+ // ── Backup ──
129
+ {
130
+ "g": "Backup", "icon": "πŸ’Ύ",
131
+ "k": "HF_TOKEN",
132
+ "lbl": "HuggingFace token β€” enables state backup to a private dataset",
133
+ "type": "password", "secret": 1, "common": 1
134
+ },
135
+ {
136
+ "g": "Backup", "icon": "πŸ’Ύ",
137
+ "k": "BACKUP_DATASET_NAME",
138
+ "lbl": "Name of the HF dataset used for backups",
139
+ "type": "text", "ph": "huggingmes-backup", "common": 1
140
+ },
141
+ {
142
+ "g": "Backup", "icon": "πŸ’Ύ",
143
+ "k": "SYNC_INTERVAL",
144
+ "lbl": "Backup sync interval (seconds)",
145
+ "type": "number", "ph": "600"
146
+ },
147
+
148
+ // ── Telegram ──
149
+ {
150
+ "g": "Telegram", "icon": "πŸ“±",
151
+ "k": "TELEGRAM_BOT_TOKEN",
152
+ "lbl": "Telegram bot token from @BotFather",
153
+ "type": "password", "secret": 1, "common": 1
154
+ },
155
+ {
156
+ "g": "Telegram", "icon": "πŸ“±",
157
+ "k": "TELEGRAM_ALLOWED_USERS",
158
+ "lbl": "Allowed Telegram user IDs (comma-separated)",
159
+ "type": "text", "ph": "123456789,987654321", "common": 1
160
+ },
161
+ {
162
+ "g": "Telegram", "icon": "πŸ“±",
163
+ "k": "TELEGRAM_MODE",
164
+ "lbl": "Telegram update mode",
165
+ "type": "select",
166
+ "options": ["webhook", "polling"],
167
+ "ph": "webhook"
168
+ },
169
+ {
170
+ "g": "Telegram", "icon": "πŸ“±",
171
+ "k": "TELEGRAM_WEBHOOK_URL",
172
+ "lbl": "Override webhook URL (auto-detected from SPACE_HOST if blank)",
173
+ "type": "text", "ph": "https://your-space.hf.space/telegram"
174
+ },
175
+ {
176
+ "g": "Telegram", "icon": "πŸ“±",
177
+ "k": "TELEGRAM_BASE_URL",
178
+ "lbl": "Custom Telegram API base URL (for proxies)",
179
+ "type": "text", "ph": "https://proxy.example.com/bot"
180
+ },
181
+
182
+ // ── Terminal ──
183
+ {
184
+ "g": "Terminal", "icon": "πŸ’»",
185
+ "k": "DEV_MODE",
186
+ "lbl": "Enable JupyterLab terminal (on by default)",
187
+ "type": "toggle", "ph": "true", "common": 1
188
+ },
189
+ {
190
+ "g": "Terminal", "icon": "πŸ’»",
191
+ "k": "JUPYTER_TOKEN",
192
+ "lbl": "Override terminal password (defaults to GATEWAY_TOKEN)",
193
+ "type": "password", "secret": 1
194
+ },
195
+ {
196
+ "g": "Terminal", "icon": "πŸ’»",
197
+ "k": "JUPYTER_ROOT_DIR",
198
+ "lbl": "JupyterLab root directory",
199
+ "type": "text", "ph": "/opt/data/workspace"
200
+ },
201
+
202
+ // ── Providers ──
203
+ {
204
+ "g": "Providers", "icon": "πŸ”‘",
205
+ "k": "ANTHROPIC_API_KEY",
206
+ "lbl": "Anthropic API key",
207
+ "type": "password", "secret": 1
208
+ },
209
+ {
210
+ "g": "Providers", "icon": "πŸ”‘",
211
+ "k": "OPENAI_API_KEY",
212
+ "lbl": "OpenAI API key",
213
+ "type": "password", "secret": 1
214
+ },
215
+ {
216
+ "g": "Providers", "icon": "πŸ”‘",
217
+ "k": "GOOGLE_API_KEY",
218
+ "lbl": "Google / Gemini API key",
219
+ "type": "password", "secret": 1
220
+ },
221
+ {
222
+ "g": "Providers", "icon": "πŸ”‘",
223
+ "k": "GEMINI_API_KEY",
224
+ "lbl": "Gemini API key (alias for GOOGLE_API_KEY)",
225
+ "type": "password", "secret": 1
226
+ },
227
+ {
228
+ "g": "Providers", "icon": "πŸ”‘",
229
+ "k": "OPENROUTER_API_KEY",
230
+ "lbl": "OpenRouter API key",
231
+ "type": "password", "secret": 1
232
+ },
233
+ {
234
+ "g": "Providers", "icon": "πŸ”‘",
235
+ "k": "DEEPSEEK_API_KEY",
236
+ "lbl": "DeepSeek API key",
237
+ "type": "password", "secret": 1
238
+ },
239
+ {
240
+ "g": "Providers", "icon": "πŸ”‘",
241
+ "k": "XAI_API_KEY",
242
+ "lbl": "xAI (Grok) API key",
243
+ "type": "password", "secret": 1
244
+ },
245
+ {
246
+ "g": "Providers", "icon": "πŸ”‘",
247
+ "k": "HERMES_INFERENCE_PROVIDER",
248
+ "lbl": "Force Hermes inference provider (overrides auto-detect)",
249
+ "type": "select",
250
+ "options": ["auto", "anthropic", "openai", "gemini", "openrouter", "huggingface", "custom", "deepseek", "xai"],
251
+ "ph": "auto"
252
+ },
253
+ {
254
+ "g": "Providers", "icon": "πŸ”‘",
255
+ "k": "CUSTOM_BASE_URL",
256
+ "lbl": "Custom OpenAI-compatible base URL",
257
+ "type": "text", "ph": "https://your-api.example.com/v1"
258
+ },
259
+ {
260
+ "g": "Providers", "icon": "πŸ”‘",
261
+ "k": "CUSTOM_API_KEY",
262
+ "lbl": "API key for the custom provider",
263
+ "type": "password", "secret": 1
264
+ },
265
+ {
266
+ "g": "Providers", "icon": "πŸ”‘",
267
+ "k": "CUSTOM_PROVIDER",
268
+ "lbl": "Provider name for custom endpoints",
269
+ "type": "text", "ph": "custom"
270
+ },
271
+ {
272
+ "g": "Providers", "icon": "πŸ”‘",
273
+ "k": "CUSTOM_MODEL_CONTEXT_LENGTH",
274
+ "lbl": "Context length for custom model",
275
+ "type": "number", "ph": "131072"
276
+ },
277
+ {
278
+ "g": "Providers", "icon": "πŸ”‘",
279
+ "k": "CUSTOM_MODEL_MAX_TOKENS",
280
+ "lbl": "Max output tokens for custom model",
281
+ "type": "number", "ph": "8192"
282
+ },
283
+
284
+ // ── Cloudflare ──
285
+ {
286
+ "g": "Cloudflare", "icon": "☁️",
287
+ "k": "CLOUDFLARE_WORKERS_TOKEN",
288
+ "lbl": "Cloudflare Workers API token (for Telegram proxy setup)",
289
+ "type": "password", "secret": 1
290
+ },
291
+ {
292
+ "g": "Cloudflare", "icon": "☁️",
293
+ "k": "CLOUDFLARE_PROXY_URL",
294
+ "lbl": "Cloudflare proxy URL for Telegram (if already deployed)",
295
+ "type": "text", "ph": "https://your-worker.your-subdomain.workers.dev"
296
+ },
297
+ {
298
+ "g": "Cloudflare", "icon": "☁️",
299
+ "k": "CLOUDFLARE_PROXY_DEBUG",
300
+ "lbl": "Enable Cloudflare proxy debug logging",
301
+ "type": "toggle", "ph": "false"
302
+ },
303
+
304
+ // ── Advanced ──
305
+ {
306
+ "g": "Advanced", "icon": "βš™οΈ",
307
+ "k": "WEBHOOK_URL",
308
+ "lbl": "URL to POST a JSON notification on gateway (re)start",
309
+ "type": "text", "ph": "https://..."
310
+ },
311
+ {
312
+ "g": "Advanced", "icon": "βš™οΈ",
313
+ "k": "GATEWAY_READY_TIMEOUT",
314
+ "lbl": "Seconds to wait for gateway API port before failing",
315
+ "type": "number", "ph": "120"
316
+ },
317
+ {
318
+ "g": "Advanced", "icon": "βš™οΈ",
319
+ "k": "API_SERVER_PORT",
320
+ "lbl": "Hermes gateway internal API port",
321
+ "type": "number", "ph": "8642"
322
+ },
323
+ {
324
+ "g": "Advanced", "icon": "βš™οΈ",
325
+ "k": "DASHBOARD_PORT",
326
+ "lbl": "Hermes dashboard internal port",
327
+ "type": "number", "ph": "9119"
328
+ },
329
+ {
330
+ "g": "Advanced", "icon": "βš™οΈ",
331
+ "k": "HERMES_BACKGROUND_NOTIFICATIONS",
332
+ "lbl": "Background process notification level",
333
+ "type": "select",
334
+ "options": ["result", "progress", "none"],
335
+ "ph": "result"
336
+ },
337
+ {
338
+ "g": "Advanced", "icon": "βš™οΈ",
339
+ "k": "TELEGRAM_WEBHOOK_SECRET",
340
+ "lbl": "Secret token for Telegram webhook validation (auto-generated if blank)",
341
+ "type": "password", "secret": 1
342
+ }
343
+ ];
344
+
345
+ // ── Runtime (shared with HuggingClaw env-builder) ──
346
+
347
+ const BUNDLE_KEY = 'HUGGINGMES_ENV_BUNDLE';
348
+
349
+ const $ = id => document.getElementById(id);
350
+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
351
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
352
+ }[c]));
353
+ const safeKey = k => /^[A-Z_][A-Z0-9_]*$/.test(k) && ![BUNDLE_KEY, 'ENV_BUNDLE'].includes(k);
354
+
355
+ function encodeBundle(obj) {
356
+ const j = JSON.stringify(obj);
357
+ let b = '';
358
+ for (const x of new TextEncoder().encode(j)) b += String.fromCharCode(x);
359
+ return btoa(b).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
360
+ }
361
+
362
+ function decodeBundle(raw) {
363
+ try {
364
+ raw = String(raw || '').trim();
365
+ if (!raw) return {};
366
+ if (raw.includes(BUNDLE_KEY + '=')) raw = raw.split(BUNDLE_KEY + '=').pop().trim();
367
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) raw = raw.slice(1, -1);
368
+ if (raw.startsWith('{')) return JSON.parse(raw);
369
+ const p = raw + '='.repeat((4 - raw.length % 4) % 4);
370
+ const b = atob(p.replace(/-/g, '+').replace(/_/g, '/'));
371
+ const bytes = Uint8Array.from(b, c => c.charCodeAt(0));
372
+ return JSON.parse(new TextDecoder().decode(bytes));
373
+ } catch { return {}; }
374
+ }
375
+
376
+ function parseEnv(text) {
377
+ text = String(text || '').trim();
378
+ if (!text) return {};
379
+ if (text.startsWith('{') || /^[A-Za-z0-9_-]{20,}$/.test(text) || text.includes(BUNDLE_KEY + '=')) {
380
+ return decodeBundle(text);
381
+ }
382
+ const out = {};
383
+ for (let line of text.split(/\r?\n/)) {
384
+ line = line.trim();
385
+ if (!line || line.startsWith('#')) continue;
386
+ if (line.startsWith('export ')) line = line.slice(7).trim();
387
+ const i = line.indexOf('=');
388
+ if (i < 1) continue;
389
+ const key = line.slice(0, i).trim();
390
+ let val = line.slice(i + 1).trim();
391
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
392
+ if (safeKey(key)) out[key] = val;
393
+ }
394
+ return out;
395
+ }
396
+
397
+ function showToast(msg = 'Copied!') {
398
+ const t = $('toast');
399
+ t.textContent = msg;
400
+ t.classList.add('show');
401
+ setTimeout(() => t.classList.remove('show'), 1500);
402
+ }
403
+
404
+ let activeGroup = 'All';
405
+ let customCount = 0;
406
+ const GROUPS = ['All', ...[...new Set(FIELDS.map(f => f.g))], 'Custom Env'];
407
+
408
+ function renderSidebar() {
409
+ const sb = $('sidebar');
410
+ sb.innerHTML = '<div class="sb-label">Groups</div>';
411
+ GROUPS.forEach(g => {
412
+ const btn = document.createElement('button');
413
+ btn.className = 'nav-btn' + (activeGroup === g ? ' active' : '');
414
+ btn.dataset.group = g;
415
+ const id = 'nc_' + g.replace(/\W/g, '_');
416
+ btn.innerHTML = `<span class="nav-icon">${ICONS[g] || 'πŸ“'}</span><span class="nav-label">${esc(g)}</span><span class="nav-count" id="${id}">0</span>`;
417
+ btn.onclick = () => { activeGroup = g; renderSidebar(); filter(); };
418
+ sb.appendChild(btn);
419
+ });
420
+ }
421
+
422
+ function renderOptionsHTML(field) {
423
+ if (field.options_key === 'LLM_MODEL') {
424
+ const groups = MODEL_CATALOGS.LLM_MODEL || {};
425
+ return Object.entries(groups).map(([group, items]) => {
426
+ const options = items.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
427
+ return `<optgroup label="${esc(group)}">${options}</optgroup>`;
428
+ }).join('');
429
+ }
430
+ const src = field.options || MODEL_CATALOGS[field.options_key] || [];
431
+ if (Array.isArray(src)) return src.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
432
+ return '';
433
+ }
434
+
435
+ function defaultValueFor(field) {
436
+ if (field.type === 'toggle') {
437
+ const on = String(field.ph ?? '').toLowerCase();
438
+ return ['1', 'true', 'yes', 'on', 'enabled'].includes(on) ? 'true' : 'false';
439
+ }
440
+ if (field.type === 'select') return String(field.ph ?? '');
441
+ return '';
442
+ }
443
+
444
+ function valueControlHTML(field) {
445
+ const key = esc(field.k);
446
+ const placeholder = esc(field.ph || field.lbl || '');
447
+ const isSecret = !!field.secret;
448
+ const isTextarea = field.type === 'textarea';
449
+ const hasPicker = !!field.options_key || Array.isArray(field.options);
450
+ const inputType = isSecret ? 'password' : (field.type === 'number' ? 'number' : 'text');
451
+
452
+ let control = '';
453
+ if (field.type === 'toggle') {
454
+ const initial = defaultValueFor(field);
455
+ control = `<div class="toggle-shell" data-toggle-row="1" data-field="${key}">
456
+ <input type="hidden" data-key="${key}" value="${initial}">
457
+ <button type="button" class="tog ${initial === 'true' ? 'on' : ''}" data-toggle="${key}">${initial === 'true' ? 'On' : 'Off'}</button>
458
+ </div>`;
459
+ } else if (isTextarea) {
460
+ control = `<textarea data-key="${key}" placeholder="${placeholder}" spellcheck="false"></textarea>`;
461
+ } else {
462
+ control = `<input type="${inputType}" data-key="${key}" placeholder="${placeholder}" spellcheck="false"/>`;
463
+ }
464
+
465
+ if (!hasPicker) return control;
466
+
467
+ return `<div class="picker-shell" data-picker-shell="${key}" data-picker-mode="single">
468
+ <div class="picker-row">
469
+ <select class="picker-select" data-pick-for="${key}" aria-label="${esc(field.lbl || field.k)} presets">
470
+ <option value="">Choose preset…</option>
471
+ ${renderOptionsHTML(field)}
472
+ <option value="__custom__">Custom…</option>
473
+ </select>
474
+ <button type="button" class="mini-btn" data-custom-for="${key}">+ Custom</button>
475
+ <button type="button" class="mini-btn" data-clear-for="${key}">Clear</button>
476
+ </div>
477
+ ${control}
478
+ </div>`;
479
+ }
480
+
481
+ function cardHTML(f) {
482
+ const badge = f.secret
483
+ ? '<span class="badge badge-s">secret</span>'
484
+ : '<span class="badge badge-f">safe</span>';
485
+ return `<div class="env-card" data-row data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '')).toLowerCase())}">
486
+ <div class="card-top">
487
+ <input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
488
+ <div class="card-info">
489
+ <div class="card-key">${esc(f.k)}</div>
490
+ <div class="card-lbl">${esc(f.lbl || '')}</div>
491
+ </div>
492
+ ${badge}
493
+ </div>
494
+ <div class="card-input">${valueControlHTML(f)}</div>
495
+ </div>`;
496
+ }
497
+
498
+ function addCustomRow(key = '', val = '', enabled = false) {
499
+ const id = customCount++;
500
+ const row = document.createElement('div');
501
+ row.className = 'custom-row';
502
+ row.dataset.customRow = id;
503
+ row.dataset.enabled = enabled ? '1' : '0';
504
+ row.innerHTML = `
505
+ <input data-ck="${id}" placeholder="CUSTOM_ENV_NAME" value="${esc(key)}">
506
+ <input data-cv="${id}" placeholder="value" value="${esc(val)}">
507
+ <button class="tog${enabled ? ' on' : ''}">${enabled ? 'On' : 'Off'}</button>`;
508
+ $('customRows').appendChild(row);
509
+ row.querySelectorAll('input').forEach(el => el.addEventListener('input', refresh));
510
+ row.querySelector('button').onclick = () => {
511
+ const on = row.dataset.enabled !== '1';
512
+ row.dataset.enabled = on ? '1' : '0';
513
+ row.querySelector('button').textContent = on ? 'On' : 'Off';
514
+ row.querySelector('button').classList.toggle('on', on);
515
+ refresh();
516
+ };
517
+ }
518
+
519
+ function getFieldValueInput(key) { return document.querySelector(`[data-key="${CSS.escape(key)}"]`); }
520
+
521
+ function setFieldValue(key, value) {
522
+ const el = getFieldValueInput(key);
523
+ if (el) el.value = value ?? '';
524
+ }
525
+
526
+ function appendCsvValue(existing, next) {
527
+ const parts = String(existing || '').split(',').map(s => s.trim()).filter(Boolean);
528
+ const val = String(next || '').trim();
529
+ if (!val) return parts.join(', ');
530
+ if (!parts.includes(val)) parts.push(val);
531
+ return parts.join(', ');
532
+ }
533
+
534
+ function collect() {
535
+ const obj = {};
536
+ document.querySelectorAll('[data-key]').forEach(el => {
537
+ const key = el.dataset.key;
538
+ if (!key || !safeKey(key)) return;
539
+ const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
540
+ if (!chk || !chk.checked) return;
541
+ const val = String(el.value ?? '').trim();
542
+ if (val) obj[key] = val;
543
+ });
544
+ document.querySelectorAll('[data-custom-row]').forEach(row => {
545
+ const id = row.dataset.customRow;
546
+ const key = (row.querySelector(`[data-ck="${id}"]`)?.value || '').trim();
547
+ const val = (row.querySelector(`[data-cv="${id}"]`)?.value || '').trim();
548
+ if (row.dataset.enabled === '1' && safeKey(key) && val) obj[key] = val;
549
+ });
550
+ return obj;
551
+ }
552
+
553
+ function refresh() {
554
+ const obj = collect();
555
+ const keys = Object.keys(obj).sort();
556
+ const bundle = keys.length ? encodeBundle(Object.fromEntries(keys.map(k => [k, obj[k]]))) : '';
557
+ $('bundleOut').value = bundle;
558
+ $('envLineOut').value = bundle ? `${BUNDLE_KEY}=${bundle}` : '';
559
+ const s = $('summary');
560
+ if (keys.length) {
561
+ s.innerHTML = `<strong>${keys.length}</strong> variable${keys.length > 1 ? 's' : ''} selected<div class="sum-keys">${keys.map(k => `<span class="sum-key">${esc(k)}</span>`).join('')}</div>`;
562
+ } else {
563
+ s.innerHTML = 'No variables selected yet.';
564
+ }
565
+ updateCounts();
566
+ }
567
+
568
+ function markSelected() {
569
+ document.querySelectorAll('[data-row]').forEach(r => r.classList.toggle('selected', !!r.querySelector('[data-check]')?.checked));
570
+ }
571
+
572
+ function updateCounts() {
573
+ document.querySelectorAll('[id^="nc_"]').forEach(el => el.textContent = '0');
574
+ const byGrp = {};
575
+ document.querySelectorAll('[data-check]:checked').forEach(ch => {
576
+ const g = ch.closest('[data-row]')?.dataset.group;
577
+ if (g) byGrp[g] = (byGrp[g] || 0) + 1;
578
+ });
579
+ const custOn = document.querySelectorAll('[data-custom-row][data-enabled="1"]').length;
580
+ const total = Object.values(byGrp).reduce((a, b) => a + b, 0) + custOn;
581
+ const allEl = document.getElementById('nc_All'); if (allEl) allEl.textContent = total;
582
+ Object.entries(byGrp).forEach(([g, c]) => {
583
+ const el = document.getElementById('nc_' + g.replace(/\W/g, '_'));
584
+ if (el) el.textContent = c;
585
+ });
586
+ const custEl = document.getElementById('nc_Custom_Env'); if (custEl) custEl.textContent = custOn;
587
+ }
588
+
589
+ function filter() {
590
+ const q = $('search').value.trim().toLowerCase();
591
+ document.querySelectorAll('.sec[data-section]').forEach(sec => {
592
+ const grp = sec.dataset.section;
593
+ const gMatch = activeGroup === 'All' || activeGroup === grp;
594
+ if (!gMatch) { sec.classList.add('sec-hidden'); return; }
595
+ let any = false;
596
+ sec.querySelectorAll('[data-row]').forEach(card => {
597
+ const m = !q || card.dataset.search.includes(q);
598
+ card.classList.toggle('hidden', !m);
599
+ if (m) any = true;
600
+ });
601
+ sec.classList.toggle('sec-hidden', !any);
602
+ });
603
+ const cs = $('customSec');
604
+ if (cs) cs.style.display = (activeGroup === 'All' || activeGroup === 'Custom Env') ? '' : 'none';
605
+ document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.group === activeGroup));
606
+ }
607
+
608
+ function clearForm() {
609
+ document.querySelectorAll('[data-check]').forEach(c => c.checked = false);
610
+ document.querySelectorAll('[data-key]').forEach(el => {
611
+ if (el.closest('[data-toggle-row]')) {
612
+ el.value = 'false';
613
+ const btn = el.closest('.toggle-shell')?.querySelector('[data-toggle]');
614
+ if (btn) { btn.textContent = 'Off'; btn.classList.remove('on'); }
615
+ return;
616
+ }
617
+ el.value = '';
618
+ });
619
+ $('customRows').innerHTML = '';
620
+ customCount = 0;
621
+ addCustomRow();
622
+ }
623
+
624
+ function applyObj(obj, replace = false) {
625
+ if (replace) clearForm();
626
+ for (const [key, val] of Object.entries(obj || {})) {
627
+ if (!safeKey(key)) continue;
628
+ const inp = getFieldValueInput(key);
629
+ const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
630
+ if (inp && chk) {
631
+ inp.value = val;
632
+ chk.checked = true;
633
+ const btn = inp.closest('[data-toggle-row]')?.querySelector('[data-toggle]');
634
+ if (btn) {
635
+ const on = String(val).trim().toLowerCase() === 'true';
636
+ btn.textContent = on ? 'On' : 'Off';
637
+ btn.classList.toggle('on', on);
638
+ inp.value = on ? 'true' : 'false';
639
+ }
640
+ } else {
641
+ addCustomRow(key, val, true);
642
+ }
643
+ }
644
+ markSelected(); filter(); refresh();
645
+ }
646
+
647
+ function autoCheck(key) {
648
+ const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
649
+ if (chk && !chk.checked) { chk.checked = true; markSelected(); }
650
+ }
651
+
652
+ function handlePickerChange(sel) {
653
+ const key = sel.dataset.pickFor;
654
+ const value = sel.value;
655
+ if (!key || !value || value === '__custom__') { if (value === '__custom__') sel.value = ''; return; }
656
+ const inp = getFieldValueInput(key);
657
+ if (!inp) return;
658
+ inp.value = value;
659
+ sel.value = '';
660
+ autoCheck(key);
661
+ refresh();
662
+ }
663
+
664
+ function promptCustomModel(btn) {
665
+ const key = btn.dataset.customFor;
666
+ const inp = getFieldValueInput(key);
667
+ if (!inp) return;
668
+ const text = prompt('Enter a custom value', '');
669
+ if (text === null) return;
670
+ const val = String(text).trim();
671
+ if (!val) return;
672
+ inp.value = val;
673
+ autoCheck(key);
674
+ refresh();
675
+ }
676
+
677
+ function resetPickerField(btn) {
678
+ const key = btn.dataset.clearFor;
679
+ const inp = getFieldValueInput(key);
680
+ if (!inp) return;
681
+ if (inp.closest('[data-toggle-row]')) {
682
+ inp.value = 'false';
683
+ const toggleBtn = inp.closest('.toggle-shell')?.querySelector('[data-toggle]');
684
+ if (toggleBtn) { toggleBtn.textContent = 'Off'; toggleBtn.classList.remove('on'); }
685
+ } else {
686
+ inp.value = '';
687
+ }
688
+ refresh();
689
+ }
690
+
691
+ function toggleField(key) {
692
+ const inp = getFieldValueInput(key);
693
+ if (!inp) return;
694
+ const on = String(inp.value || '').trim().toLowerCase() !== 'true';
695
+ inp.value = on ? 'true' : 'false';
696
+ const btn = inp.closest('.toggle-shell')?.querySelector('[data-toggle]');
697
+ if (btn) { btn.textContent = on ? 'On' : 'Off'; btn.classList.toggle('on', on); }
698
+ const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
699
+ if (chk) { chk.checked = on; markSelected(); }
700
+ refresh();
701
+ }
702
+
703
+ function bindFieldEvents() {
704
+ document.querySelectorAll('[data-check]').forEach(el => el.addEventListener('change', () => { markSelected(); refresh(); }));
705
+ document.querySelectorAll('[data-key]').forEach(el => el.addEventListener('input', refresh));
706
+ document.querySelectorAll('[data-toggle]').forEach(btn => btn.addEventListener('click', () => toggleField(btn.dataset.toggle)));
707
+ document.querySelectorAll('[data-pick-for]').forEach(sel => sel.addEventListener('change', () => handlePickerChange(sel)));
708
+ document.querySelectorAll('[data-custom-for]').forEach(btn => btn.addEventListener('click', () => promptCustomModel(btn)));
709
+ document.querySelectorAll('[data-clear-for]').forEach(btn => btn.addEventListener('click', () => resetPickerField(btn)));
710
+ }
711
+
712
+ function renderSections() {
713
+ const grouped = {};
714
+ FIELDS.forEach(f => { (grouped[f.g] ||= []).push(f); });
715
+ const wrap = $('sections');
716
+ wrap.innerHTML = '';
717
+ Object.entries(grouped).forEach(([grp, items]) => {
718
+ const sec = document.createElement('div');
719
+ sec.className = 'sec';
720
+ sec.dataset.section = grp;
721
+ sec.innerHTML = `<div class="sec-header">
722
+ <span class="sec-icon">${ICONS[grp] || 'πŸ“'}</span>
723
+ <span class="sec-title">${esc(grp)}</span>
724
+ <div class="sec-line"></div>
725
+ </div>
726
+ <div class="cards">${items.map(cardHTML).join('')}</div>`;
727
+ wrap.appendChild(sec);
728
+ });
729
+ bindFieldEvents();
730
+ }
731
+
732
+ function copyText(text) {
733
+ return navigator.clipboard.writeText(text).then(
734
+ () => showToast('Copied βœ“'),
735
+ () => {
736
+ const ta = document.createElement('textarea');
737
+ ta.value = text;
738
+ ta.style.position = 'fixed';
739
+ ta.style.left = '-9999px';
740
+ document.body.appendChild(ta);
741
+ ta.select();
742
+ document.execCommand('copy');
743
+ ta.remove();
744
+ showToast('Copied βœ“');
745
+ }
746
+ );
747
+ }
748
+
749
+ // ��─ Init ──
750
+ renderSidebar();
751
+ renderSections();
752
+ addCustomRow();
753
+ filter();
754
+ refresh();
755
+
756
+ // ── Events ──
757
+ $('search').oninput = filter;
758
+ $('selectCommon').onclick = () => {
759
+ document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
760
+ markSelected(); refresh();
761
+ };
762
+ $('selectVisible').onclick = () => {
763
+ document.querySelectorAll('.sec:not(.sec-hidden) [data-row]:not(.hidden) [data-check]').forEach(c => c.checked = true);
764
+ markSelected(); refresh();
765
+ };
766
+ $('clearAll').onclick = () => { clearForm(); markSelected(); filter(); refresh(); };
767
+ $('applyImport').onclick = () => {
768
+ try { applyObj(parseEnv($('importText').value), true); showToast('Imported βœ“'); }
769
+ catch (e) { showToast('Import failed'); alert(e.message); }
770
+ };
771
+ $('importText').addEventListener('paste', () => {
772
+ setTimeout(() => {
773
+ try {
774
+ const val = $('importText').value.trim();
775
+ if (!val) return;
776
+ applyObj(parseEnv(val), true);
777
+ showToast('Auto-imported βœ“');
778
+ } catch (e) { showToast('Import failed'); }
779
+ }, 0);
780
+ });
781
+ $('importText').addEventListener('input', () => {
782
+ const val = $('importText').value.trim();
783
+ if (!val) return;
784
+ const looksLikeEnv = val.includes('=') || val.startsWith('{') || /^[A-Za-z0-9_\-]{20,}$/.test(val);
785
+ if (looksLikeEnv) {
786
+ try { applyObj(parseEnv(val), true); } catch (e) { /* silent */ }
787
+ }
788
+ });
789
+ $('addCustom').onclick = () => addCustomRow();
790
+ $('applyBundle').onclick = () => {
791
+ try { applyObj(decodeBundle($('bundleOut').value), true); showToast('Bundle applied βœ“'); }
792
+ catch (e) { showToast('Invalid bundle'); }
793
+ };
794
+ $('copyBundle').onclick = () => copyText($('bundleOut').value);
795
+ $('copyEnvLine').onclick = () => copyText($('envLineOut').value);
796
+ $('copyJson').onclick = () => copyText(JSON.stringify(collect(), null, 2));
health-server.js CHANGED
@@ -537,6 +537,7 @@ function renderDashboard(data) {
537
  <div class="hero-buttons">
538
  <a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes Agent β†’</a>
539
  <a class="hero-action secondary" href="/terminal/" target="_blank" rel="noopener noreferrer">Open Terminal β†’</a>
 
540
  </div>
541
  <section class="overview">
542
  ${tiles}
@@ -588,6 +589,30 @@ const server = http.createServer(async (req, res) => {
588
  return;
589
  }
590
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  if (path === "/") {
592
  const data = await statusPayload();
593
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
 
537
  <div class="hero-buttons">
538
  <a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes Agent β†’</a>
539
  <a class="hero-action secondary" href="/terminal/" target="_blank" rel="noopener noreferrer">Open Terminal β†’</a>
540
+ <a class="hero-action secondary" href="/env-builder" target="_blank" rel="noopener noreferrer">ENV Builder β†’</a>
541
  </div>
542
  <section class="overview">
543
  ${tiles}
 
589
  return;
590
  }
591
 
592
+ if (path === "/env-builder" || path === "/env-builder/") {
593
+ try {
594
+ const html = fs.readFileSync(require("path").join(__dirname, "env-builder.html"), "utf8");
595
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
596
+ res.end(html);
597
+ } catch (e) {
598
+ res.writeHead(404, { "content-type": "text/plain" });
599
+ res.end("env-builder.html not found");
600
+ }
601
+ return;
602
+ }
603
+
604
+ if (path === "/env-builder.js") {
605
+ try {
606
+ const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
607
+ res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
608
+ res.end(js);
609
+ } catch (e) {
610
+ res.writeHead(404, { "content-type": "text/plain" });
611
+ res.end("env-builder.js not found");
612
+ }
613
+ return;
614
+ }
615
+
616
  if (path === "/") {
617
  const data = await statusPayload();
618
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });