Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
590 lines (589 loc) · 360 KB
/
Copy pathindex.html
File metadata and controls
590 lines (589 loc) · 360 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
10BC8
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
<!doctype html> <html lang="de"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="dark light"> <script src="/i18n.js"></script> <script>try{var t=localStorage.getItem("pb_theme"),d=localStorage.getItem("pb_design"),e=document.documentElement;if(d==="klar"||d==="kompakt"||d==="kompakt-std"||d==="kontrast"||d==="cockpit"||d==="mischpult")e.setAttribute("data-design",d);else d="";if(t==="light"||t==="dark")e.setAttribute("data-theme",t);else if(d==="klar")e.setAttribute("data-theme","light")}catch(e){}</script> <title>IRL4YOU BOX</title> <style>:root{--bg:#0f172a;--card:#1e293b;--text:#f8fafc;--muted:#a8b4c7;--line:#50647f;--accent:#06b6d4;--ok:#22c55e;--warn:#f59e0b;--crit:#ef4444;--bar:#06b6d4;--blue:#3b82f6;--shade:rgba(2,6,23,.45)}:root{color-scheme:dark}:root[data-theme="light"]{color-scheme:light;--bg:#f1f5f9;--card:#ffffff;--text:#0f172a;--muted:#475569;--line:#cbd5e1;--accent:#0e7490;--ok:#16a34a;--warn:#d97706;--crit:#dc2626;--bar:#0891b2;--blue:#2563eb;--shade:rgba(15,23,42,.16)}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.4 -apple-system,system-ui,sans-serif}h1.hl{display:inline-grid;align-items:center}.hl .hlogo,.hl .htxt{grid-area:1/1;display:inline-flex;align-items:center}.hl .hlogo svg{display:block;height:26px;width:26px}@keyframes hlA{0%,46%{opacity:1}50%,96%{opacity:0}100%{opacity:1}}@keyframes hlB{0%,46%{opacity:0}50%,96%{opacity:1}100%{opacity:0}}.hl .hlogo{animation:hlA 60s linear infinite}.hl .htxt{animation:hlB 60s linear infinite}body.streaming .hl .htxt{display:none}body.streaming .hl .hlogo{animation:none;opacity:1}@media(prefers-reduced-motion:reduce){.hl .hlogo,.hl .htxt{animation:none;opacity:1}h1.hl{display:inline-flex;gap:8px}.hl .hlogo,.hl .htxt{grid-area:auto}}header{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;margin:12px auto 0;max-width:calc(980px - 32px);width:calc(100% - 32px);border:1px solid var(--line);border-radius:14px;background:var(--card);position:sticky;top:8px;z-index:50;box-shadow:0 0 0 8px var(--bg),0 12px 12px -4px var(--shade)}.card{scroll-margin-top:calc(var(--hdrh,64px) + 20px)}h1{font-size:17px;margin:0;color:var(--accent)}.badge{font-size:12px;padding:3px 8px;border-radius:99px;background:var(--line);color:var(--muted)}.badge.demo{background:var(--warn);color:#0f172a}.livebtn{font-size:12px;font-weight:700;padding:3px 12px;border-radius:99px;border:1px solid var(--line);background:var(--card);color:var(--text);cursor:pointer;letter-spacing:.02em}.livebtn:not(.on):not(.busy){background:var(--warn);border-color:var(--warn);color:#0f172a}.livebtn.on{background:var(--crit);border-color:var(--crit);color:#fff}.livebtn.busy{background:var(--warn);border-color:var(--warn);color:#0f172a}.livebtn:disabled{opacity:.55;cursor:not-allowed}header .hdr button,header .hdr>.badge{box-sizing:border-box;min-height:30px;padding:4px 12px;font-size:13px;line-height:20px;border-radius:8px;border:1px solid var(--line);margin:0}header .hdr>.badge{display:inline-flex;align-items:center;background:var(--card);color:var(--muted);font-weight:400}header .hdr>.badge.demo{background:var(--warn);border-color:var(--warn);color:#0f172a;font-weight:600}header .hdr button.foldm{font-size:16px;padding:2px 12px}header .hdr .livebtn{font-weight:700}main{max-width:980px;margin:0 auto;padding:16px;display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr))}.card{min-width:0;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px}.card h2{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:0 0 10px}.wide{grid-column:1/-1}#prev_img,#prev_vid{display:block;width:100%;max-width:640px;aspect-ratio:16/9;margin-top:10px;background:#000;border-radius:10px;object-fit:contain}#prev_box[hidden]{display:none}#prev_msg:empty{display:none}.prevkeep{display:flex;align-items:flex-start;gap:8px;margin-top:8px;font-size:12px;color:var(--muted);line-height:1.4}.prevkeep input{flex:none;margin-top:2px}.prevkeep[hidden]{display:none}.prevset{padding:2px 8px;line-height:1.3}html.nohead :is(#c_chat,#c_prev,#c_deck){position:relative}html.nohead :is(#c_chat,#c_prev,#c_deck)>summary{position:absolute;top:8px;right:8px;margin:0!important;z-index:3;pointer-events:none;gap:6px}html.nohead :is(#c_chat,#c_prev,#c_deck)>summary>.sumh,html.nohead :is(#c_chat,#c_deck)>summary>:is(.sig,.camdots){display:none}html.nohead :is(#c_chat,#c_prev,#c_deck)>summary :is(button,.prevctl){pointer-events:auto}html.nohead #c_prev .prevctl{background:var(--card);border-radius:10px;padding:2px 4px;opacity:.94}html.nohead #c_prev{min-height:48px}html.nohead #c_prev.prevrun>summary{visibility:hidden;opacity:0;transition:opacity .2s}html.nohead #c_prev.prevrun.showctl>summary{visibility:visible;opacity:1}html.nohead #c_prev>:is(.ph,.prevkeep,.autolbl){margin-right:150px}.prevctl{display:flex;align-items:center;gap:8px;margin-left:auto}.prevctl button.small{padding:2px 12px;line-height:1.3}#prev_img,#prev_vid{margin-top:0}details.card#c_prev[open]>summary{margin-bottom:8px}.row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:4px 0}.big{font-size:26px;font-weight:600}.unit{font-size:13px;color:var(--muted);font-weight:400}.bar{height:8px;background:var(--line);border-radius:4px;overflow:hidden}.bar>i{display:block;height:100%;background:var(--bar);transition:width .5s}.prog{margin:10px 0 2px}.prog .bar{height:10px}.prog .bar>i{background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.22) 0 8px,transparent 8px 16px);background-size:22.6px 22.6px;animation:progmove 1s linear infinite}@keyframes progmove{to{background-position:22.6px 0}}@media (prefers-reduced-motion:reduce){.prog .bar>i{animation:none}}.progline{display:flex;justify-content:space-between;gap:10px;margin-top:4px;font-size:13px}.progline .progtxt{min-width:0;overflow-wrap:anywhere}.progline .progel{white-space:nowrap;font-variant-numeric:tabular-nums}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;background:var(--ok)}.dot.off{background:var(--crit)}.dot.wait{background:var(--warn)}header .hdr .updbtn{border-color:var(--warn);background:rgba(245,158,11,.12);color:var(--text);font-weight:600}.updbtn .dot{margin:0 6px 0 0;width:9px;height:9px;vertical-align:baseline}@media(max-width:620px){.updbtn .u1{display:none}}#alerts{display:grid;gap:8px}#alerts:empty{display:none}.alert{padding:9px 12px;border-radius:8px;font-size:14px;border-left:4px solid var(--warn);background:var(--bg)}.alert.crit{border-color:var(--crit)}.alert .abtn{float:right;margin:-2px -4px 0 8px;padding:0 6px;border:0;background:none;color:var(--muted);font-size:20px;line-height:1;cursor:pointer;border-radius:6px}.alert .abtn:hover,.alert .abtn:focus-visible{color:var(--text);background:color-mix(in srgb,var(--muted) 18%,transparent)}.alert.ok{border-color:var(--ok);color:var(--muted)}.muted{color:var(--muted)}.ph{font-size:12px;color:var(--muted);margin-top:8px}.cam{padding:10px 0;border-bottom:1px solid var(--line)}.cam:last-of-type{border-bottom:0}.cam .top{display:flex;gap:6px 12px;align-items:center;flex-wrap:wrap}.cam .top .cl{flex:1 1 200px;min-width:0}.cam .grip{display:none;cursor:grab;touch-action:none;-webkit-touch-callout:none;user-select:none;-webkit-user-select:none;color:var(--muted);padding:4px 8px 4px 0;margin-right:2px;font-size:16px;line-height:1;letter-spacing:-3px;vertical-align:middle}.cam .grip:hover,.cam .grip:focus-visible{color:var(--text)}.cam.dragging{background:var(--card);box-shadow:0 4px 14px var(--shade);position:relative;z-index:5;border-radius:8px}#cams{position:relative}.camdrop{position:absolute;left:0;right:0;height:5px;margin-top:-3px;background:var(--accent);border-radius:3px;z-index:30;pointer-events:none;box-shadow:0 0 0 2px var(--bg),0 0 10px var(--accent)}body.camdrag,body.camdrag *{cursor:grabbing!important;user-select:none;-webkit-user-select:none}.cam .top .cn{flex:0 1 270px;min-width:0}.cam .top .cr{flex:0 0 auto;margin-left:auto;white-space:nowrap}.cam .top .dyn{display:inline-block;min-width:9.6em;text-align:right;font-variant-numeric:tabular-nums}@media(max-width:620px){.cam .grip{display:inline-block;font-size:28px;letter-spacing:-4px;padding:8px 18px 8px 2px;margin-right:4px}.cam .top .cr .mv{display:none}.cam .top .cr{flex:1 1 100%;display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:8px;margin-left:0;white-space:normal}.cam .top .cr .dyn{flex:1 1 100%;min-width:0;text-align:left}}.camdots .dot{flex:none}.deckfit{overflow:hidden;border-radius:10px;max-width:100%}.deckwrap iframe{display:block;border:0;height:auto;min-height:0;width:100%;transform-origin:0 0;background:var(--card)}#c_chat>summary,#c_deck>summary{flex-wrap:nowrap;gap:6px}#c_chat>summary .sumh,#c_deck>summary .sumh{white-space:nowrap}.sig.stale,.sig.stale.ok,.sig.stale.warn,.sig.stale.crit{color:var(--muted)}.chatoptbtn{margin-left:auto;padding:2px 9px;line-height:1.3}.chatopts .accrow{flex:0 0 100%;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-bottom:6px;margin-bottom:2px;border-bottom:1px solid var(--line)}.chatopts{display:flex;flex-wrap:wrap;gap:6px 16px;margin:0 0 10px;padding:8px 10px;border:1px solid var(--line);border-radius:10px;font-size:13px}.chatopts label{display:inline-flex;align-items:center;gap:6px}.chatwrap{position:relative}.sig{display:inline-flex;align-items:center;gap:7px;margin-left:10px;color:var(--muted);vertical-align:middle}#livebox .row .sig{margin:0 0 0 16px}#livebox .row .sig .sgv{display:none}.sig.ok{color:var(--ok)}.sig.warn{color:var(--warn)}.sig.crit{color:var(--crit)}.sig .sigdot{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor;animation:sigpulse 1.8s ease-in-out infinite;transition:color .35s ease}.sig.warn .sigdot{animation-duration:1.1s}.sig.crit .sigdot{animation-duration:.7s}.sig svg{display:block;width:80px;height:20px;overflow:visible}.sig .sgv{font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;min-width:3ch;color:currentColor}.sig .sgs{display:flex;flex-direction:column;justify-content:space-between;height:20px;min-width:20px;margin-right:-2px;font-size:7px;font-weight:700;line-height:6px;text-align:right;color:var(--muted);font-variant-numeric:tabular-nums;overflow:hidden}.sig .sg{fill:none;stroke:color-mix(in srgb,var(--text) 20%,transparent);stroke-width:1;vector-effect:non-scaling-stroke}.sig polyline{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke;filter:drop-shadow(0 0 2px currentColor)}.camdots{display:inline-flex;align-items:center;gap:6px;margin-left:auto;min-width:0}.camdots .dot{width:9px;height:9px;margin:0}.camdots:not(:empty)+.chatoptbtn{margin-left:0}@keyframes sigpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.82)}}@media(prefers-reduced-motion:reduce){.sig .sigdot{animation:none}}@media(min-width:461px){.sig{height:28px}.sig svg{height:28px}.sig .sgs{height:28px;font-size:8px;line-height:9px;min-width:22px;overflow:visible}}@media(max-width:460px){.sig{gap:5px;margin-left:6px}.sig .sgs{display:none}.sig svg{width:60px}.camdots{gap:4px}.camdots .dot{width:8px;height:8px}}.qrline{margin-top:4px;display:flex;align-items:center;gap:6px;opacity:.75}.qrline:hover,.qrline:focus-within{opacity:1}.qrline select{width:auto;min-width:0;padding:1px 4px;font-size:12px;height:24px}.qrbox{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}.qri{display:flex;flex-direction:column;align-items:flex-start;gap:2px;max-width:100%;font-size:11px}.qri svg{width:150px;max-width:100%;height:auto;border-radius:3px}.qrbox.big{flex-direction:column;gap:16px}.qrbox.big .qri svg{width:340px}@media(max-width:620px){.qrbox{flex-direction:column;gap:18px}.qri{font-size:14px;gap:4px}.qri svg{width:min(100%,340px)}}.cam .netnote,.cam .netlock{margin:3px 0 0;font-size:11px;line-height:1.3;color:var(--muted)}.cam .netnote.ok{color:var(--ok)}.cam .netnote.bad{color:var(--crit)}.cam code{display:block;margin-top:4px;font-size:12px;color:var(--muted);word-break:break-all}.tag{font-size:11px;padding:2px 7px;border-radius:99px;background:var(--line);color:var(--muted);margin-left:6px}.cam a.ren{color:var(--text);text-decoration:none;border-bottom:1px dotted var(--muted)}.dot.unk{background:var(--muted)}.add{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr));gap:8px;margin-top:12px}.add>label{display:flex;flex-direction:column;justify-content:flex-end;gap:4px;min-width:0}.add>label[hidden]{display:none}.dlrow{display:flex;gap:8px;align-items:center}.dlrow input[type=range]{flex:1;min-width:0;padding:0}.dlrow input[type=number]{width:84px;flex:none}.add.dset{align-items:center}.sech{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:18px 0 6px}.wform{display:grid;grid-template-columns:minmax(130px,.7fr) 1.4fr 1.4fr;gap:10px;align-items:end;margin-top:10px}.wform label{display:flex;flex-direction:column;gap:4px;min-width:0}.wform select,.wform input{width:100%;height:38px;box-sizing:border-box}#srtlacard .autolbl{margin-bottom:12px}.bkform{grid-template-columns:1fr 1fr}.bkwhat{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:4px 28px;margin:6px 0 2px}.bkwhat .bkwh{font-weight:650;margin:8px 0 2px}.bkwhat ul{margin:0;padding-left:20px}.bkwhat li{margin:3px 0}.bkwhat .bkno li::marker{color:var(--crit)}.bkwhat .bkyes li::marker{color:var(--ok)}.bkplan label.logopt{margin:6px 0}.bkplan .bkok{color:var(--ok)}.bkplan .bkbad{color:var(--crit)}.bkres div{margin:4px 0}.ibtn{display:inline-block;margin-left:8px;width:22px;height:22px;padding:0;border-radius:50%;border:1px solid var(--accent);background:transparent;color:var(--accent);font:italic 700 13px/20px Georgia,serif;text-transform:none;letter-spacing:0;cursor:pointer;vertical-align:middle}.swinfo{display:inline-block;margin-left:8px;width:22px;height:22px;padding:0;border-radius:50%;border:1px solid var(--accent);background:transparent;color:var(--accent);font:italic 700 13px/20px Georgia,serif;text-transform:none;letter-spacing:0;cursor:pointer;vertical-align:middle}.ibtn[aria-expanded=true]{background:var(--accent);color:#0f172a}.ibrow{display:block;margin:0 0 4px}.ibrow .ibtn{margin-left:0}details:not([open])>summary .ibtn{display:none}details>summary .ibtn{margin-left:10px;vertical-align:middle;text-transform:none;letter-spacing:0}.hlp:not(.open){display:none}html.nohelp :is(.ibtn,.ibrow,.helpbtn,.hlp){display:none!important}.helpbtn{width:22px;height:22px;padding:0;border-radius:50%;border:1px solid var(--accent);background:transparent;color:var(--accent);font:italic 700 13px/20px Georgia,serif;text-transform:none;letter-spacing:0;cursor:pointer;vertical-align:middle}.helpbtn[aria-expanded=true]{background:var(--accent);color:#0f172a}@media(max-width:620px){.wform{grid-template-columns:1fr}}.wstat{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);margin-top:8px}.wstat .wdot{width:10px;height:10px;border-radius:50%;background:var(--line);flex:none}.wstat.ok .wdot{background:var(--ok)}.wstat.bad .wdot{background:var(--crit)}.wstat .wgrow{flex:1;min-width:160px}.wstat label{display:flex;gap:8px;align-items:center;white-space:nowrap}#wificard a{color:var(--accent)}.pvbox{position:relative;width:100%;max-width:640px;background:linear-gradient(135deg,#1d3b6b,#0e2a4a 60%,#0a1f38);border:1px solid var(--line);overflow:hidden;user-select:none;margin-top:6px}.pvmain{position:absolute;left:12px;top:8px;font-size:13px;color:#cbd5e1}.pvpip{touch-action:none;position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:12px;color:#fff;cursor:grab;overflow:hidden}.pvpip .pvin{position:absolute}.pvpip .pvrim{position:absolute;left:0;top:0;right:0;bottom:0;box-sizing:border-box;pointer-events:none}.pvpip b,.pvpip span{position:relative}.pvpip b{font-size:18px}.pvpip span{max-width:96%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pvpip.drag{cursor:grabbing;outline:2px solid var(--warn)}.pvpip.off{outline:1px dashed rgba(255,255,255,.6)}.pv1 .pvin{background:linear-gradient(135deg,#0891b2,#164e63)}.pv2 .pvin{background:linear-gradient(135deg,#059669,#064e3b)}.pv3 .pvin{background:linear-gradient(135deg,#d97706,#78350f)}.pvs{margin-top:8px;border-top:1px solid var(--line);padding-top:6px}.pvs>summary{cursor:pointer;color:var(--muted);font-size:13px}.pvs .pvs-sum{font-size:12px;opacity:.8}.pvs .pvs-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 10px;margin:4px 0}.pvs .pvs-grid input[type=number]{width:100%}.pvs input[type=color]{width:100%;height:30px;padding:2px;box-sizing:border-box}.pvs .sech{margin:10px 0 2px}.pvs .pvs-info{font-size:12px;margin:2px 0 6px}.wlist{display:grid;gap:4px;max-height:260px;overflow:auto}.wrow{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:12px;align-items:center;padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--text);text-decoration:none}.wrow:hover{border-color:var(--accent)}.wrow.cur{border-color:var(--ok)}.wname{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wsig{font-variant-numeric:tabular-nums;white-space:nowrap}.wsec{color:var(--muted);font-size:13px;white-space:nowrap}.wuse{color:var(--ok);font-size:13px}@media(max-width:560px){.wrow{grid-template-columns:minmax(0,1fr) auto}.wsec,.wuse{grid-column:1/-1}}.camcols{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(210px,100%),1fr));gap:10px;margin-top:12px}.camcol{display:flex;flex-direction:column;gap:8px;min-width:0;border:1px solid var(--line);border-radius:10px;padding:10px;background:var(--bg)}.camcol[hidden]{display:none}.camcol.slotoff{display:none}.batt{font-size:13px;white-space:nowrap}.batt.stale{opacity:.6}#camlights .row .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#camlights.hasbatt{display:grid;grid-template-columns:minmax(0,1fr) 4.3em 5.4em;column-gap:14px;align-items:baseline}#camlights.hasbatt .row,#camlights.hasbatt .hrow{display:contents}#camlights.hasbatt .row>span{padding:4px 0}#camlights.hasbatt .row>span:last-child{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}#camlights .battcell{text-align:center;white-space:nowrap;font-variant-numeric:tabular-nums}#camlights .hrow .mid{text-align:center}.cam .camnet{display:block;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cam .camnet select{padding:2px 6px;font-size:12px;width:100%;max-width:100%}.camcol h3{margin:0;font-size:13px;color:var(--accent)}.camcol .opt{color:var(--muted);font-weight:400}.camcol>label{display:flex;flex-direction:column;gap:4px;min-width:0}.camcol>label[hidden]{display:none}.camcol .fixed{min-height:2.2em;display:flex;align-items:center}.srvrow{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.srvrow select{flex:1 1 200px}input,select,button{font:inherit;min-width:0;max-width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--line);background:var(--bg);color:var(--text)}input:focus,select:focus{outline:2px solid var(--accent);outline-offset:0}button{background:var(--bar);color:#0f172a;font-weight:600;border-color:transparent;cursor:pointer}button.mv{background:transparent;color:var(--muted);border-color:var(--line);padding:4px 8px;font-size:12px;line-height:1}button.mv:disabled{opacity:.4;cursor:default}button.del{background:transparent;color:var(--crit);border-color:var(--line);padding:4px 10px;font-size:13px}details.card>summary{cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;list-style:none}details.card>summary::-webkit-details-marker{display:none}details.card>summary .sumh{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--accent)}details.card>summary .sumh::before,html[data-design] details.card>summary .sumh::before{color:var(--warn);border-color:var(--warn)}details.card>summary .sumh::before{content:"\25B8 "}details.card[open]>summary .sumh::before{content:"\25BE "}details.card[open]>summary{margin-bottom:10px}.statgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:12px;margin-top:4px}.statbox{min-width:0;border:1px solid var(--line);border-radius:10px;padding:8px 10px 12px;background:var(--bg)}.statbox .sech{margin:0 0 4px}.statbox .big{font-size:24px;line-height:1.1}.seg{display:flex;flex-wrap:wrap;gap:6px}.seg button{background:transparent;color:var(--text);border:1px solid var(--line);border-radius:8px;padding:6px 12px;font-size:14px;font-weight:400;cursor:pointer;display:inline-flex;align-items:center}.seg button:hover{border-color:var(--accent)}.seg button.on{background:var(--bar);border-color:var(--bar);color:#0f172a;font-weight:600;cursor:default}.seg button .dot{margin-right:8px}.seg button.on .dot{box-shadow:0 0 0 2px #0f172a}.statbox .row{padding:2px 0}#net.hasrows{display:grid;grid-template-columns:minmax(0,1fr) minmax(3em,auto) auto .9em minmax(2.5em,auto) auto;align-items:baseline}#net.hasrows .row{display:contents}#net.hasrows .row>span{padding:2px 0;white-space:nowrap}#net.hasrows .row>.nm{min-width:0;overflow:hidden;text-overflow:ellipsis;padding-right:8px}#net.hasrows .row>.num{text-align:right;font-variant-numeric:tabular-nums}#net.hasrows .row>.u{padding-left:4px}#net.hasrows .row>.sep{text-align:center}#net.hasrows .sum>span{box-shadow:0 -1px 0 var(--line)}.hdr{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}.foldd{display:inline-flex;gap:6px}.foldm{display:none}.smic{display:none;font-size:16px;line-height:1}button.small{padding:4px 10px;font-size:13px}#lang_sel{padding:4px 4px;font-size:13px;width:auto;min-width:48px;flex:none}#theme_btn{padding:4px 8px;line-height:0}#theme_btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}button.foldm{font-size:17px;line-height:1;padding:3px 10px}@media(max-width:620px){.smtx{display:none}.smic{display:inline}#sm_btn{padding:3px 10px}.foldd{display:none}.foldm{display:inline-block}header{gap:8px}}#mfoot{display:none}.mf-live{flex:1 1 76px;min-width:68px;min-height:54px;padding:0 4px;white-space:nowrap;border-radius:12px;border:1px solid var(--ok);background:var(--ok);color:#06210f;font-size:17px;font-weight:700;letter-spacing:.02em;cursor:pointer}#mfoot:has(#mf_tools[hidden]) .mf-live{flex:0 0 auto;min-width:112px;padding:0 22px}#hdr_sig,#hdr_more{display:none}#hdr_sig .sig{margin-left:0}#hdr_sig .camdots{margin-left:8px}#hdr_more{padding:0;width:26px;min-width:0;align-items:center;justify-content:center}#hdr_more svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;transition:transform .2s}#hdr_more[aria-expanded=true] svg{transform:rotate(180deg)}@media(prefers-reduced-motion:reduce){#hdr_more svg{transition:none}}.mf-live.on{background:var(--crit);border-color:var(--crit);color:#fff}.mf-live.busy{background:var(--warn);border-color:var(--warn);color:#0f172a}.mf-live:disabled{opacity:.55;cursor:not-allowed}#mf_tools{display:flex;gap:6px;min-width:0;flex:0 1 auto}.mf-t{flex:0 1 60px;min-width:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:54px;padding:2px;border-radius:12px;border:2px solid var(--line);background:var(--bg);color:var(--text);font-family:inherit;font-size:10px;font-weight:700;line-height:1.1;cursor:pointer;touch-action:manipulation;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.mf-t svg{width:22px;height:22px;flex:none}.mf-t span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mf-aud span{white-space:normal;text-align:center;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.mf-t:active{filter:brightness(1.3)}.mf-cam.live{color:var(--ok)}.mf-cam.hid{color:var(--blue)}.mf-cam.ismain{border-color:var(--blue)}.mf-cam.off{opacity:.55;border-style:dashed}.mf-cam.off.live{color:var(--text)}.mf-aud{color:var(--text)}.mf-info{flex:0 0 34px;min-height:54px;padding:0;border:0;background:transparent;color:var(--muted,var(--text));font-size:22px;line-height:1;cursor:pointer}.mf-msg{white-space:pre-line}.mf-msg[data-info="1"]{white-space:pre;overflow:hidden}.mf-aud.muted{color:var(--crit);border-color:var(--crit)}.mf-msg{position:absolute;left:0;right:0;bottom:calc(100% + 10px);padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--card);color:var(--text);font-size:14px;box-shadow:0 8px 24px rgba(2,6,23,.6)}@media(max-width:620px){header{flex-wrap:nowrap;padding:6px 10px;gap:6px;width:calc(100% - 16px)}main{padding:6px 8px 16px;gap:10px}header{margin-top:max(4px,env(safe-area-inset-top));top:4px}header h1{font-size:15px;white-space:nowrap}header .hdr{flex-wrap:nowrap;gap:5px;min-width:0}h1.hl{flex:0 0 auto}h1.hl .htxt{display:none}h1.hl .hlogo{animation:none;opacity:1}header .hdr button:not([hidden]),header .hdr>.badge{display:inline-flex;align-items:center;justify-content:center;min-width:36px;min-height:34px;padding:3px 10px;line-height:1;text-align:center;white-space:nowrap}header .hdr select{min-height:34px;text-align:center;text-align-last:center}header .hdr .updbtn{font-size:0;padding:3px 9px}header .hdr .updbtn .dot{margin:0;font-size:13px}html:not(.mfperm) #hdr_live{display:none!important}body.streaming:not(.hdr-open) #hdr_sig{display:inline-flex;align-items:center;min-width:0}body.streaming #hdr_more{display:inline-flex}body.streaming:not(.hdr-open) header .hdr>*:not(#hdr_sig):not(#hdr_more){display:none!important}body.streaming #hdr_sig .sig .sgs{display:flex}body.streaming #hdr_sig .sig{gap:5px;height:28px}body.streaming #hdr_sig .sig svg{height:28px}body.streaming #hdr_sig .sig .sgs{height:28px;font-size:8px;line-height:9px;min-width:22px;overflow:visible}body.streaming header h1{font-size:13px}#livecard{padding:0;border:0;background:transparent;box-shadow:none;margin:0}#livecard #livebox>.row,#livecard #swapbar,#livecard #liveserver,#livecard #livehealth{display:none}#livecard :is(.ph,.err,.pending):empty{display:none}#livecard:not(:has(:is(.ph,.err,.pending):not(:empty):not([hidden]))){display:none}#mfoot{display:flex;position:fixed;left:8px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom));z-index:60;gap:6px;padding:6px;background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:0 0 0 8px var(--bg),0 env(safe-area-inset-bottom) 0 8px var(--bg),0 -12px 12px -4px var(--shade)}#mfoot{transition:transform .22s ease,visibility 0s}html.mfhide #mfoot{transform:translateY(calc(100% + 32px + env(safe-area-inset-bottom)));visibility:hidden;transition:transform .22s ease,visibility 0s .22s}html.mfperm body{padding-bottom:calc(16px + env(safe-area-inset-bottom))}body{padding-bottom:calc(var(--mfh,72px) + 20px + env(safe-area-inset-bottom))}.toast{bottom:calc(var(--mfh,72px) + 28px + env(safe-area-inset-bottom))}html.mfhide .toast{bottom:calc(20px + env(safe-area-inset-bottom))}html.nofootinfo #mf_info{display:none!important}html.mf2 #mfoot{display:grid;grid-auto-flow:column;grid-template-rows:repeat(2,minmax(60px,auto));grid-auto-columns:minmax(60px,auto);gap:8px;padding:8px;align-items:stretch}html.mf2 #mf_tools{display:contents}html.mf2 :is(.mf-live,.mf-aud,.mf-info){grid-row:1/span 2}html.mf2 .mf-live{min-width:92px;min-height:0;padding:0 8px;font-size:22px;flex:none}html.mf2 .mf-t{flex:none;min-width:0;min-height:0;width:auto;padding:4px 6px;gap:3px;font-size:12px;border-radius:14px}html.mf2 .mf-t svg{width:28px;height:28px}html.mf2 .mf-info{flex:none;min-height:0;min-width:40px;font-size:28px}html.mfleft #mfoot,html.mfright #mfoot{top:calc(8px + env(safe-area-inset-top));left:8px;right:auto;bottom:calc(8px + env(safe-area-inset-bottom));width:80px;flex-direction:column-reverse;align-items:stretch;gap:6px;padding:6px;overflow-y:auto;overscroll-behavior:contain;box-shadow:0 0 0 6px var(--bg),0 0 16px var(--shade)}html.mfright #mfoot{left:auto;right:8px}html.mfleft #mf_tools,html.mfright #mf_tools{display:contents}html.mfleft :is(.mf-live,.mf-t,.mf-info),html.mfright :is(.mf-live,.mf-t,.mf-info){flex:1 1 0;width:100%;min-width:0;min-height:44px;max-height:170px}html.mfleft .mf-live,html.mfright .mf-live{flex:1.4 1 0}html.mfleft .mf-info,html.mfright .mf-info{flex:none;max-height:44px}html.mfleft .mf-live,html.mfright .mf-live{padding:0 2px}html.mfleft .mf-live,html.mfright .mf-live{order:0}html.mfleft .mf-aud,html.mfright .mf-aud{order:1}html.mfleft .mf-cam[data-slot="0"],html.mfright .mf-cam[data-slot="0"]{order:2}html.mfleft .mf-cam[data-slot="1"],html.mfright .mf-cam[data-slot="1"]{order:3}html.mfleft .mf-cam[data-slot="2"],html.mfright .mf-cam[data-slot="2"]{order:4}html.mfleft .mf-cam[data-slot="3"],html.mfright .mf-cam[data-slot="3"]{order:5}html.mfleft .mf-info,html.mfright .mf-info{order:9}html.mfleft .mf-t,html.mfright .mf-t{font-size:11px}html.mfleft .mf-t svg,html.mfright .mf-t svg{width:30px;height:30px}html.mfleft .mf-info,html.mfright .mf-info{min-height:38px}html.mfleft body{padding-left:96px}html.mfright body{padding-right:96px}html.mfleft body,html.mfright body,html.mfleft.mfperm body,html.mfright.mfperm body{padding-bottom:calc(16px + env(safe-area-inset-bottom))}html.mfhide.mfleft #mfoot,html.mfhide.mfright #mfoot{transform:none;visibility:visible}html.mfleft .toast,html.mfright .toast,html.mfhide.mfleft .toast,html.mfhide.mfright .toast{bottom:calc(20px + env(safe-area-inset-bottom))}html.mfleft #mf_msg{position:fixed;left:96px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom))}html.mfright #mf_msg{position:fixed;left:8px;right:96px;bottom:calc(8px + env(safe-area-inset-bottom))}}@media(max-width:374px){.mf-live{min-width:60px}.mf-t{min-width:38px}}@media(max-width:374px){header .hdr button:not([hidden]),header .hdr>.badge{min-width:30px;padding:3px 6px}header .hdr .updbtn{min-width:28px;padding:3px 5px}header .hdr{gap:4px}}@media(max-width:339px){body.streaming #hdr_sig .sig svg{width:44px}body.streaming #hdr_sig .camdots{margin-left:5px;gap:3px}#mfoot{gap:4px;padding:5px}#mf_tools{gap:4px}.mf-live{min-width:52px;font-size:15px}.mf-t{min-width:34px}header{padding:5px 8px;gap:4px}header h1{font-size:13px}header .hdr{gap:4px}header .hdr button{padding:3px 7px}}#rcard a.btnlink{display:inline-block;padding:8px 12px;border-radius:8px;background:var(--bar);color:#0f172a;font-weight:600;text-decoration:none}#rcard #r_https a.btnlink{background:var(--warn);color:#0f172a;font-weight:700;padding:10px 16px}#r_apps a.btnlink{background:transparent;color:var(--text);border:1px solid var(--line);font-weight:400}#r_url{word-break:break-all}#rcard ol{margin:6px 0 0 18px;padding:0;color:var(--muted);font-size:13px}#swcard[open]>summary .sumh::before{content:"\25BE "}#swcard[open]>summary{margin-bottom:10px}#swcard .swpick{margin-top:12px}#sw_notes,#sw_hres{max-height:26em;overflow:auto;margin:10px 0;padding:6px 14px 10px;border:1px solid var(--line);border-radius:10px;background:var(--bg);font-size:14px;line-height:1.5}#sw_notes h4,#sw_hres h4{margin:12px 0 4px;font-size:14px;color:var(--accent);letter-spacing:.02em}#sw_notes h4:first-child,#sw_hres h4:first-child{margin-top:6px}#sw_notes ul,#sw_hres ul{margin:4px 0 8px;padding-left:20px}#sw_notes li,#sw_hres li{margin:5px 0}#sw_notes p,#sw_hres p{margin:4px 0}#sw_notes code,#sw_hres code{font-size:12.5px;padding:1px 5px;border-radius:5px;background:var(--card)}#rp_title,#rp_desc{width:100%;box-sizing:border-box;margin:6px 0}#rp_out a.rpgo{display:inline-block;margin:6px 0;padding:8px 14px;border-radius:10px;background:var(--accent);color:#000;font-weight:600;text-decoration:none}.rpcase{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:baseline;padding:6px 0;border-top:1px solid var(--line)}.rpcase.done .rpt{text-decoration:line-through;opacity:.7}.rpcase code{font-size:12.5px}.rpcase .rpd{color:var(--muted);font-size:13px}#sw_hist{margin:8px 0}#sw_hq{width:100%;box-sizing:border-box}#sw_hcount{margin:6px 0 0}#sw_hres mark{background:var(--warn);color:#000;border-radius:3px;padding:0 1px}.toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:80;max-width:calc(100% - 32px);padding:10px 16px;border:1px solid var(--ok);border-radius:10px;background:var(--card);color:var(--text);box-shadow:0 8px 24px rgba(2,6,23,.6);font-size:14px}#sw_notes{white-space:pre-wrap;border-left:3px solid var(--line);padding-left:10px;margin:10px 0}#updcard[open]>summary .sumh::before{content:"\25BE "}#updcard[open]>summary{margin-bottom:10px}.badge.attn{background:var(--warn);color:#0f172a}.badge.ok{background:var(--line);color:var(--muted)}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.dcam{border:1px solid var(--line);border-radius:10px;margin:8px 0;padding:0 12px}.dcam>summary{padding:10px 0;cursor:pointer;display:flex;gap:10px;align-items:center;justify-content:space-between;list-style:none}.dcam>summary::-webkit-details-marker{display:none}.dcam .dname{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dcam .dright{white-space:nowrap;flex:0 0 auto;font-size:13px}.dcam .dbody{padding:0 0 12px}.dcam .dsub{font-size:13px;margin-bottom:4px}.dcam label.f,.dcam div.f{display:block;font-size:12px;color:var(--muted);margin:8px 0 2px;min-width:0}.dcam label.f input,.dcam label.f select,.dcam div.f select{width:100%}.dcam .flh{display:flex;justify-content:space-between;align-items:baseline;gap:8px;min-height:20px}.dcam .flb{background:none;border:0;padding:0 2px;font-size:12px;font-weight:400;color:var(--accent);cursor:pointer;white-space:nowrap}.dcam .flb.crit{color:var(--crit)}.dcam .flb:disabled{opacity:.4;cursor:default}.dcam .d-nearmsg{font-size:12px;color:var(--warn);margin-top:4px}.dcam .d-nearmsg:empty{display:none}.camreconn{flex:none;padding:2px 8px;line-height:1.2}.cam .camnet.withbtn{display:flex;align-items:center;gap:4px;overflow:visible}.cam .camnet.withbtn select{flex:1 1 0;min-width:0;width:auto}.dcam .dnet{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:12px}@media (max-width:560px){.dcam .dnet{grid-template-columns:minmax(0,1fr)}}.dcam .dnet[hidden],.dcam div.f[hidden],.dcam label.f[hidden]{display:none}.dcam .chk{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:14px}.dcam .dsec{border-top:1px solid var(--line);margin-top:10px;padding-top:6px}.dcam .dsec>summary{cursor:pointer;font-size:13px;padding:4px 0}.subsec>summary{display:block;cursor:pointer;list-style:none;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:14px 0 6px}.subsec>summary::-webkit-details-marker{display:none}.subsec>summary::before{content:"\25B8 "}.subsec[open]>summary::before{content:"\25BE "}.livehealth a.unsavedlnk,.alert a.unsavedlnk{color:var(--crit);font-weight:600;text-decoration:underline;cursor:pointer}.dcam .dgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(120px,100%),1fr));gap:8px}.dscan{display:flex;justify-content:space-between;align-items:center;gap:8px;border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin-top:6px}button.sec{background:transparent;color:var(--text);border:1px solid var(--line);font-weight:400}button.warnbtn{background:var(--warn)}button:disabled{opacity:.45;cursor:not-allowed}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;color:var(--muted);max-height:220px;overflow:auto;margin:8px 0 0}.upd-line{display:flex;justify-content:space-between;padding:3px 0}.livehealth{font-size:12px;margin-top:8px;opacity:.85}.livehealth .ok{color:var(--ok)}.livehealth .warn{color:var(--warn)}.livehealth .crit,.livehealth .err{color:var(--crit)}details.locked>summary{cursor:not-allowed;opacity:.6}body.sm .sens{display:none!important}.dangerbtn{background:var(--crit);border-color:var(--crit);color:#fff}.wcard{margin-bottom:6px}.hsrow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:6px 0 2px 18px}.hsrow .muted{font-size:13px}.hsw{min-width:64px;font-weight:700}.hsw.on{background:var(--ok);border-color:var(--ok);color:#06210f}.hspanel{margin:6px 0 10px 18px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg)}.pwshow{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:22px;font-weight:600;letter-spacing:.05em;color:var(--fg,inherit);margin:6px 0;padding:8px 12px;border:1px solid var(--line);border-radius:8px;user-select:all;word-break:break-all;display:inline-block;cursor:pointer}.pwshow.copied{border-color:var(--ok)}.pwshow.copied::after,.pwshow.copyfail::after{font-family:inherit;font-size:13px;font-weight:400;letter-spacing:0}.pwshow.copied::after{content:" ✓ kopiert";color:var(--ok)}.pwshow.copyfail::after{content:" markiert: bitte Strg/Cmd+C";color:var(--warn)}.smon{background:var(--warn);color:#111}.lockhint{font-size:12px;color:var(--muted);font-weight:400}.foot a{color:var(--muted);text-decoration:underline}.footlinks{margin-top:6px}.pending{margin-top:10px;padding:8px 10px;border:1px solid var(--warn);border-radius:8px;font-size:13px}.pending button{margin-left:8px;padding:4px 10px}dialog#rdlg{background:var(--card);color:var(--text);border:1px solid var(--warn);border-radius:12px;max-width:min(420px,90vw);padding:18px}dialog#rdlg::backdrop{background:rgba(0,0,0,.6)}#livecard #liveerr,#livecard #livewhy{min-height:0;margin-top:8px}#livecard #liveerr:empty,#livecard #livewhy:empty{display:none}.livesrv{font-size:12px;margin-top:3px}#livebox .liverow{flex-wrap:wrap}#livebox .liverow .livesrv{margin:0 0 0 auto}.livetemp{margin-left:16px;font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}.liveup{margin-left:2px;font-size:22px;font-weight:700;font-variant-numeric:tabular-nums}.liveup small{font-size:13px;font-weight:400;color:var(--muted)}.logopt{display:block;margin:10px 0;line-height:1.4}.logopt input{margin-right:6px}.autolbl{margin-top:10px;font-size:13px;color:var(--muted)}.autolbl input{margin-right:6px}[data-pbhide]{display:none!important}.designs{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(min(240px,100%),1fr));margin:8px 0}.dchoice{display:flex;gap:12px;align-items:flex-start;padding:12px;border:2px solid var(--line);border-radius:12px;cursor:pointer;background:var(--bg)}.dchoice:hover{border-color:var(--accent)}.dchoice:has(input:checked){border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,var(--bg))}.dchoice input{margin:3px 0 0;flex:none;width:20px;height:20px;accent-color:var(--accent)}.dchoice .dtx{display:grid;gap:3px;min-width:0}.dchoice .dtx b{font-size:16px}.dchoice .dtx span{font-size:13px;color:var(--muted);line-height:1.45}.dsw{display:flex;flex:none;margin-top:2px;border:1px solid var(--line);border-radius:8px;overflow:hidden}.dsw i{display:block;width:14px;height:34px}.optlist{margin:8px 0}.optgrp{margin:0 0 2px}.ordrow{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0}.ordrow.off{color:var(--muted)}.ordrow .mvs{display:inline-flex;gap:4px;flex:none}details.card[hidden]{display:none!important}.ctlhd{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin:14px 0 4px}.ctlhd b{font-size:15px}.ctlrow{display:flex;align-items:center;gap:10px;padding:7px 10px;border:1px solid var(--line);border-radius:8px;margin-top:6px;transition:background .15s,border-color .15s}.ctlrow .ctlkey{flex:1 1 40%;font-weight:600}.ctlrow select{flex:1 1 55%;min-width:0}.ctlrow.hit{background:color-mix(in srgb,var(--accent) 22%,transparent);border-color:var(--accent)}.optrow{display:flex;align-items:center;gap:8px;padding:5px 0;line-height:1.35;cursor:pointer}.optrow.sub{padding-left:26px}.optrow input{flex:none}.optrow.fixed{opacity:.6;cursor:default}.fcbtns{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0}.fcb{flex:1 1 150px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;cursor:pointer}.fcb .muted{font-size:13px;font-weight:400;color:var(--muted)}.fcb.on{background:var(--bar);color:var(--onbar,#0f172a);border-color:transparent;font-weight:600}.fcb.on .muted{color:inherit;opacity:.9}.vseg{display:inline-flex;border:1px solid var(--line);border-radius:10px;overflow:hidden;margin:2px 0 6px}.vseg button{background:transparent;color:var(--text);border:0;border-radius:0;padding:8px 18px;min-height:40px;cursor:pointer}.vseg button+button{border-left:1px solid var(--line)}.vseg button[aria-pressed=true]{background:var(--accent);color:#04202a;font-weight:600}#pb_awake[aria-pressed=true]{background:var(--accent);color:#0f172a;border-color:var(--accent)}.foot{text-align:center;font-size:12px;color:var(--muted);padding:18px 16px 28px}.err{color:var(--crit);font-size:13px;min-height:18px;margin-top:6px}.err.okmsg{color:var(--ok)}.err.okmsg::before{content:"\2713 "}html[data-design]{-webkit-text-size-adjust:100%}html[data-design] :is(button,input,select,summary,a):focus-visible{outline:3px solid var(--focus,var(--accent));outline-offset:2px}:where(html[data-design]) :is(button,select,input){transition:background-color .15s,border-color .15s,box-shadow .15s}@media(prefers-reduced-motion:reduce){:where(html[data-design]) *{transition:none!important}}html[data-design="klar"][data-theme="light"],html[data-design="klar"]:not([data-theme]){--bg:#f4f6fb;--card:#ffffff;--text:#172033;--muted:#566177;--line:#dde3ee;--ctl:#7a849a;--accent:#2457d6;--bar:#2457d6;--blue:#2457d6;--ok:#12803d;--warn:#b45309;--crit:#c62828;--onbar:#ffffff;--onwarn:#ffffff;--onok:#ffffff;--oncrit:#ffffff;--shade:rgba(23,32,51,.10);--focus:#2457d6;--r:18px;color-scheme:light}html[data-design="klar"][data-theme="dark"]{--bg:#0e1420;--card:#171f2e;--text:#eef2fa;--muted:#aab5c9;--line:#2b364b;--ctl:#6a7992;--accent:#7aa2ff;--bar:#7aa2ff;--blue:#7aa2ff;--ok:#4cd07d;--warn:#f5b04c;--crit:#ff7b72;--onbar:#0b1220;--onwarn:#1a1203;--onok:#06210f;--oncrit:#2a0a08;--shade:rgba(0,0,0,.35);--focus:#9db9ff;--r:18px;color-scheme:dark}:where(html[data-design="klar"]) body{font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;letter-spacing:.005em}:where(html[data-design="klar"]) header{border:0;border-radius:var(--r);box-shadow:0 1px 2px var(--shade),0 8px 24px var(--shade);background:color-mix(in srgb,var(--card) 88%,transparent);-webkit-backdrop-filter:saturate(1.4) blur(12px);backdrop-filter:saturate(1.4) blur(12px)}:where(html[data-design="klar"]) h1{font-size:18px;font-weight:700;letter-spacing:-.01em}:where(html[data-design="klar"]) main{gap:16px;padding:20px 16px}:where(html[data-design="klar"]) .card{border:0;border-radius:var(--r);padding:20px;box-shadow:0 1px 2px var(--shade),0 6px 20px var(--shade)}:where(html[data-design="klar"]) details.card>summary{min-height:44px}:where(html[data-design="klar"]) details.card>summary .sumh,:where(html[data-design="klar"]) .sech,:where(html[data-design="klar"]) .subsec>summary,:where(html[data-design="klar"]) .card h2{font-size:17px;font-weight:650;text-transform:none;letter-spacing:-.005em;color:var(--text)}:where(html[data-design="klar"]) .sech{margin:22px 0 8px}:where(html[data-design="klar"]) details.card>summary .sumh::before,:where(html[data-design="klar"]) .subsec>summary::before{content:"";display:inline-block;flex:none;width:.5em;height:.5em;margin:0 .7em .12em 0;border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);transform:rotate(-45deg);transition:transform .15s}:where(html[data-design="klar"]) details.card[open]>summary .sumh::before,:where(html[data-design="klar"]) .subsec[open]>summary::before,:where(html[data-design="klar"]) #swcard[open]>summary .sumh::before,:where(html[data-design="klar"]) #updcard[open]>summary .sumh::before{content:"";transform:rotate(45deg)}:where(html[data-design="klar"]) .subsec>summary{margin:18px 0 8px;min-height:40px;display:flex;align-items:center;font-size:16px;padding:12px 2px 0;border-top:1px solid var(--line)}:where(html[data-design="klar"]) .ph{font-size:14px;line-height:1.55;max-width:68ch}:where(html[data-design="klar"]) .big{font-size:30px;font-weight:650;letter-spacing:-.02em}:where(html[data-design="klar"]) .unit{font-size:14px}:where(html[data-design="klar"]) :is(input,select,button){min-height:44px;font-size:16px;border-radius:12px}:where(html[data-design="klar"]) :is(input,select){background:var(--bg);border:1.5px solid var(--ctl);padding:10px 14px}:where(html[data-design="klar"]) input:focus,:where(html[data-design="klar"]) select:focus{outline:3px solid color-mix(in srgb,var(--accent) 35%,transparent);border-color:var(--accent)}:where(html[data-design="klar"]) button{background:var(--bar);color:var(--onbar);font-weight:600;border-radius:999px;padding:10px 20px}:where(html[data-design="klar"]) button:hover:not(:disabled){filter:brightness(1.07)}:where(html[data-design="klar"]) button:active:not(:disabled){transform:translateY(1px)}:where(html[data-design="klar"]) button:disabled{opacity:.5}html[data-design] .seg button.on:disabled{opacity:1}:where(html[data-design="klar"]) button.sec{border:1.5px solid var(--ctl);font-weight:550}:where(html[data-design="klar"]) button.sec:hover:not(:disabled){border-color:var(--accent);filter:none}:where(html[data-design="klar"]) button.warnbtn{color:var(--onwarn)}:where(html[data-design="klar"]) .dangerbtn{color:var(--oncrit)}:where(html[data-design="klar"]) button.small,:where(html[data-design="klar"]) header .hdr button,:where(html[data-design="klar"]) #lang_sel{min-height:36px;font-size:14px;padding:6px 14px;border-radius:10px}:where(html[data-design="klar"]) header .hdr button.sec{border-color:var(--ctl)}:where(html[data-design="klar"]) button.mv,:where(html[data-design="klar"]) button.del{min-height:36px;min-width:40px;padding:6px 12px;font-size:14px;border-color:var(--ctl)}:where(html[data-design="klar"]) .livebtn:not(.on):not(.busy){background:var(--ok);border-color:var(--ok);color:var(--onok)}:where(html[data-design="klar"]) .livebtn.busy,:where(html[data-design="klar"]) .badge.attn,:where(html[data-design="klar"]) .badge.demo,:where(html[data-design="klar"]) header .hdr>.badge.demo{color:var(--onwarn)}:where(html[data-design="klar"]) .seg button{min-height:40px;border-radius:999px;padding:8px 16px;font-size:15px;border-color:var(--ctl)}:where(html[data-design="klar"]) .seg button.on{color:var(--onbar);border-color:var(--bar)}:where(html[data-design="klar"]) :is(.ibtn,.helpbtn)[aria-expanded=true],:where(html[data-design="klar"]) #pb_awake[aria-pressed=true],:where(html[data-design="klar"]) #rcard #r_loginlink.btnlink,:where(html[data-design="klar"]) .evchip{color:var(--onbar)}:where(html[data-design="klar"]) #r_apps a.btnlink{border-color:var(--ctl)}:where(html[data-design="klar"]) .hsw.on{color:var(--onok)}:where(html[data-design="klar"]) .statbox,:where(html[data-design="klar"]) .camcol,:where(html[data-design="klar"]) .wstat,:where(html[data-design="klar"]) .wrow,:where(html[data-design="klar"]) .chatopts,:where(html[data-design="klar"]) #sw_notes{border:1px solid var(--line);border-radius:14px;background:color-mix(in srgb,var(--bg) 60%,var(--card))}:where(html[data-design="klar"]) .statbox{padding:14px 16px 16px}:where(html[data-design="klar"]) .bar{height:10px;border-radius:99px;background:color-mix(in srgb,var(--line) 70%,transparent)}:where(html[data-design="klar"]) .bar>i{border-radius:99px}:where(html[data-design="klar"]) .badge{font-size:13px;padding:4px 10px;background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);font-weight:600}:where(html[data-design="klar"]) .badge.attn,:where(html[data-design="klar"]) .badge.demo{background:var(--warn)}:where(html[data-design="klar"]) .alert{padding:12px 14px;border-radius:12px;font-size:15px;background:color-mix(in srgb,var(--warn) 10%,var(--card))}:where(html[data-design="klar"]) .alert.ok{background:color-mix(in srgb,var(--ok) 9%,var(--card))}:where(html[data-design="klar"]) .alert.crit{background:color-mix(in srgb,var(--crit) 9%,var(--card))}:where(html[data-design="klar"]) #mfoot{border:0;border-radius:22px;box-shadow:0 -1px 0 var(--shade),0 10px 30px var(--shade),0 0 0 8px var(--bg)}:where(html[data-design="klar"]) .mf-live{border-radius:16px;min-height:56px;font-size:18px;color:var(--onok)}:where(html[data-design="klar"]) .mf-live.busy{color:var(--onwarn)}:where(html[data-design="klar"]) .mf-t{border-radius:16px;border-width:1.5px;border-color:var(--ctl);min-height:56px;background:var(--card)}:where(html[data-design="klar"]) .optrow,:where(html[data-design="klar"]) .ordrow{min-height:44px;padding:6px 2px}:where(html[data-design="klar"]) :is(.autolbl,.camnet,.batt,.wsec,.wuse,.progline){font-size:15px}@media(max-width:620px){:where(html[data-design="klar"]) .card{padding:16px}:where(html[data-design="klar"]) main{padding:8px 8px 16px;gap:12px}:where(html[data-design="klar"]) header .hdr :is(button,select){padding:6px 9px;min-height:34px}}html[data-design="kompakt"][data-theme="dark"],html[data-design="kompakt"]:not([data-theme]){--bg:#0b0f14;--card:#121821;--text:#e6ebf2;--muted:#9aa7b8;--line:#263142;--ctl:#5b6b82;--accent:#37c8e0;--bar:#37c8e0;--blue:#5aa2ff;--ok:#3ddc84;--warn:#f2b134;--crit:#ff6159;--onbar:#06151a;--onwarn:#1a1203;--onok:#06210f;--oncrit:#2a0a08;--shade:rgba(0,0,0,.4);--focus:#ffd24a;--r:6px;color-scheme:dark}html[data-design="kompakt"][data-theme="light"]{--bg:#e9edf2;--card:#ffffff;--text:#111827;--muted:#4b5668;--line:#c9d1dc;--ctl:#7b8798;--accent:#0a6a85;--bar:#0a7f9d;--blue:#1d4ed8;--ok:#127a3c;--warn:#a86100;--crit:#c0281f;--onbar:#ffffff;--onwarn:#ffffff;--onok:#ffffff;--oncrit:#ffffff;--shade:rgba(17,24,39,.12);--focus:#b45309;--r:6px;color-scheme:light}:where(html[data-design="kompakt"]) body{font:14px/1.38 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif;font-variant-numeric:tabular-nums}:where(html[data-design="kompakt"]) header{border-radius:var(--r);padding:6px 12px;max-width:min(1280px,calc(100% - 24px));width:calc(100% - 24px)}:where(html[data-design="kompakt"]) h1{font-size:15px;letter-spacing:.04em}:where(html[data-design="kompakt"]) main{max-width:1280px;gap:10px;padding:10px 12px 20px}:where(html[data-design="kompakt"]) .card{border-radius:var(--r);padding:10px 12px}:where(html[data-design="kompakt"]) details.card[open]>summary{margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--line)}:where(html[data-design="kompakt"]) details.card>summary .sumh,:where(html[data-design="kompakt"]) .sech,:where(html[data-design="kompakt"]) .subsec>summary,:where(html[data-design="kompakt"]) .card h2{font-size:12px;font-weight:700;letter-spacing:.07em}:where(html[data-design="kompakt"]) .sech{margin:12px 0 4px}:where(html[data-design="kompakt"]) .subsec>summary{margin:10px 0 4px;padding:4px 0}:where(html[data-design="kompakt"]) .ph{font-size:12.5px;line-height:1.45;max-width:90ch}:where(html[data-design="kompakt"]) .big{font-size:24px;font-weight:600}:where(html[data-design="kompakt"]) .row{padding:2px 0}:where(html[data-design="kompakt"]) :is(input,select,button){min-height:34px;padding:6px 9px;border-radius:var(--r)}:where(html[data-design="kompakt"]) :is(input,select){border-color:var(--ctl)}:where(html[data-design="kompakt"]) button{background:var(--bar);color:var(--onbar);font-weight:600}:where(html[data-design="kompakt"]) button.sec,:where(html[data-design="kompakt"]) header .hdr button{border-color:var(--ctl)}:where(html[data-design="kompakt"]) button.warnbtn{color:var(--onwarn)}:where(html[data-design="kompakt"]) .dangerbtn{color:var(--oncrit)}:where(html[data-design="kompakt"]) button.mv,:where(html[data-design="kompakt"]) button.del{min-height:30px;min-width:32px;padding:3px 8px;border-color:var(--ctl)}:where(html[data-design="kompakt"]) .livebtn:not(.on):not(.busy){background:var(--ok);border-color:var(--ok);color:var(--onok)}:where(html[data-design="kompakt"]) .livebtn.busy,:where(html[data-design="kompakt"]) .badge.attn,:where(html[data-design="kompakt"]) .badge.demo,:where(html[data-design="kompakt"]) header .hdr>.badge.demo{color:var(--onwarn)}:where(html[data-design="kompakt"]) .seg button{min-height:32px;border-radius:var(--r);padding:4px 10px;font-size:13px;border-color:var(--ctl)}:where(html[data-design="kompakt"]) .seg button.on{color:var(--onbar);border-color:var(--bar)}:where(html[data-design="kompakt"]) :is(.ibtn,.helpbtn)[aria-expanded=true],:where(html[data-design="kompakt"]) #pb_awake[aria-pressed=true],:where(html[data-design="kompakt"]) #rcard #r_loginlink.btnlink,:where(html[data-design="kompakt"]) .evchip{color:var(--onbar)}:where(html[data-design="kompakt"]) #r_apps a.btnlink{border-color:var(--ctl)}:where(html[data-design="kompakt"]) .hsw.on{color:var(--onok)}:where(html[data-design="kompakt"]) .statbox,:where(html[data-design="kompakt"]) .camcol,:where(html[data-design="kompakt"]) .wstat,:where(html[data-design="kompakt"]) .wrow,:where(html[data-design="kompakt"]) .chatopts,:where(html[data-design="kompakt"]) #sw_notes{border-radius:var(--r);padding:8px 10px}:where(html[data-design="kompakt"]) .statgrid{gap:8px}:where(html[data-design="kompakt"]) .badge{border-radius:4px;font-size:11.5px;padding:2px 7px;font-weight:600}:where(html[data-design="kompakt"]) .badge.attn,:where(html[data-design="kompakt"]) .badge.demo{background:var(--warn)}:where(html[data-design="kompakt"]) .bar{height:6px;border-radius:2px}:where(html[data-design="kompakt"]) .bar>i{border-radius:2px}:where(html[data-design="kompakt"]) .alert{border-radius:4px;padding:6px 10px;font-size:13px}:where(html[data-design="kompakt"]) #mfoot{border-radius:10px}:where(html[data-design="kompakt"]) .mf-live{border-radius:8px;color:var(--onok)}:where(html[data-design="kompakt"]) .mf-live.busy{color:var(--onwarn)}:where(html[data-design="kompakt"]) .mf-t{border-radius:8px;border-color:var(--ctl)}:where(html[data-design="kompakt"]) .optrow,:where(html[data-design="kompakt"]) .ordrow{padding:2px 0}@media(min-width:1100px){html[data-design="kompakt"] main>details.card:is(#rcard,#swcard,#updcard,#c_logs,#c_bk,#c_dev,#c_power,#c_layout){grid-column:auto}html[data-design="kompakt"] main{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){:where(html[data-design="kompakt"]) main{padding:6px 6px 14px;gap:8px}:where(html[data-design="kompakt"]) .card{padding:9px 10px}:where(html[data-design="kompakt"]) :is(button,input,select){min-height:40px}}html[data-design] :is(.ibtn,.helpbtn){min-height:0;width:26px;height:26px;padding:0;border-radius:50%;font-size:14px;line-height:24px}html[data-design="kompakt"] :is(.ibtn,.helpbtn){width:22px;height:22px;font-size:13px;line-height:20px}html[data-design] .swinfo{min-height:0;width:26px;height:26px;padding:0;border-radius:50%;font-size:14px;line-height:24px}html[data-design="kompakt"] .swinfo{width:22px;height:22px;font-size:13px;line-height:20px}html[data-design="klar"] :is(input[type=checkbox],input[type=radio]){width:22px;height:22px;min-height:0;padding:0;accent-color:var(--accent)}html[data-design="kompakt"] :is(input[type=checkbox],input[type=radio]){width:16px;height:16px;min-height:0;padding:0;accent-color:var(--accent)}html[data-design="kompakt-std"][data-theme="dark"],html[data-design="kompakt-std"]:not([data-theme]){--bg:#0f172a;--card:#1e293b;--text:#f8fafc;--muted:#a8b4c7;--line:#50647f;--ctl:#50647f;--accent:#06b6d4;--bar:#06b6d4;--blue:#3b82f6;--ok:#22c55e;--warn:#f59e0b;--crit:#ef4444;--onbar:#0f172a;--onwarn:#0f172a;--onok:#06210f;--oncrit:#ffffff;--shade:rgba(2,6,23,.45);--focus:#ffd24a;--r:6px;color-scheme:dark}html[data-design="kompakt-std"][data-theme="light"]{--bg:#f1f5f9;--card:#ffffff;--text:#0f172a;--muted:#475569;--line:#cbd5e1;--ctl:#94a3b8;--accent:#0e7490;--bar:#0891b2;--blue:#2563eb;--ok:#16a34a;--warn:#d97706;--crit:#dc2626;--onbar:#0f172a;--onwarn:#0f172a;--onok:#06210f;--oncrit:#ffffff;--shade:rgba(15,23,42,.16);--focus:#b45309;--r:6px;color-scheme:light}:where(html[data-design="kompakt-std"]) body{font:14px/1.38 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif;font-variant-numeric:tabular-nums}:where(html[data-design="kompakt-std"]) header{border-radius:var(--r);padding:6px 12px;max-width:min(1280px,calc(100% - 24px));width:calc(100% - 24px)}:where(html[data-design="kompakt-std"]) h1{font-size:15px;letter-spacing:.04em}:where(html[data-design="kompakt-std"]) main{max-width:1280px;gap:10px;padding:10px 12px 20px}:where(html[data-design="kompakt-std"]) .card{border-radius:var(--r);padding:10px 12px}:where(html[data-design="kompakt-std"]) details.card[open]>summary{margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--line)}:where(html[data-design="kompakt-std"]) details.card>summary .sumh,:where(html[data-design="kompakt-std"]) .sech,:where(html[data-design="kompakt-std"]) .subsec>summary,:where(html[data-design="kompakt-std"]) .card h2{font-size:12px;font-weight:700;letter-spacing:.07em}:where(html[data-design="kompakt-std"]) .sech{margin:12px 0 4px}:where(html[data-design="kompakt-std"]) .subsec>summary{margin:10px 0 4px;padding:4px 0}:where(html[data-design="kompakt-std"]) .ph{font-size:12.5px;line-height:1.45;max-width:90ch}:where(html[data-design="kompakt-std"]) .big{font-size:24px;font-weight:600}:where(html[data-design="kompakt-std"]) .row{padding:2px 0}:where(html[data-design="kompakt-std"]) :is(input,select,button){min-height:34px;padding:6px 9px;border-radius:var(--r)}:where(html[data-design="kompakt-std"]) :is(input,select){border-color:var(--ctl)}:where(html[data-desig
10BC8
n="kompakt-std"]) button{background:var(--bar);color:var(--onbar);font-weight:600}:where(html[data-design="kompakt-std"]) button.sec,:where(html[data-design="kompakt-std"]) header .hdr button{border-color:var(--ctl)}:where(html[data-design="kompakt-std"]) button.warnbtn{color:var(--onwarn)}:where(html[data-design="kompakt-std"]) .dangerbtn{color:var(--oncrit)}:where(html[data-design="kompakt-std"]) button.mv,:where(html[data-design="kompakt-std"]) button.del{min-height:30px;min-width:32px;padding:3px 8px;border-color:var(--ctl)}:where(html[data-design="kompakt-std"]) .livebtn.busy,:where(html[data-design="kompakt-std"]) .badge.attn,:where(html[data-design="kompakt-std"]) .badge.demo,:where(html[data-design="kompakt-std"]) header .hdr>.badge.demo{color:var(--onwarn)}:where(html[data-design="kompakt-std"]) .seg button{min-height:32px;border-radius:var(--r);padding:4px 10px;font-size:13px;border-color:var(--ctl)}:where(html[data-design="kompakt-std"]) .seg button.on{color:var(--onbar);border-color:var(--bar)}:where(html[data-design="kompakt-std"]) :is(.ibtn,.helpbtn)[aria-expanded=true],:where(html[data-design="kompakt-std"]) #pb_awake[aria-pressed=true],:where(html[data-design="kompakt-std"]) #rcard #r_loginlink.btnlink,:where(html[data-design="kompakt-std"]) .evchip{color:var(--onbar)}:where(html[data-design="kompakt-std"]) #r_apps a.btnlink{border-color:var(--ctl)}:where(html[data-design="kompakt-std"]) .hsw.on{color:var(--onok)}:where(html[data-design="kompakt-std"]) .statbox,:where(html[data-design="kompakt-std"]) .camcol,:where(html[data-design="kompakt-std"]) .wstat,:where(html[data-design="kompakt-std"]) .wrow,:where(html[data-design="kompakt-std"]) .chatopts,:where(html[data-design="kompakt-std"]) #sw_notes{border-radius:var(--r);padding:8px 10px}:where(html[data-design="kompakt-std"]) .statgrid{gap:8px}:where(html[data-design="kompakt-std"]) .badge{border-radius:4px;font-size:11.5px;padding:2px 7px;font-weight:600}:where(html[data-design="kompakt-std"]) .badge.attn,:where(html[data-design="kompakt-std"]) .badge.demo{background:var(--warn)}:where(html[data-design="kompakt-std"]) .bar{height:6px;border-radius:2px}:where(html[data-design="kompakt-std"]) .bar>i{border-radius:2px}:where(html[data-design="kompakt-std"]) .alert{border-radius:4px;padding:6px 10px;font-size:13px}:where(html[data-design="kompakt-std"]) #mfoot{border-radius:10px}:where(html[data-design="kompakt-std"]) .mf-live{border-radius:8px;color:var(--onok)}:where(html[data-design="kompakt-std"]) .mf-live.busy{color:var(--onwarn)}:where(html[data-design="kompakt-std"]) .mf-t{border-radius:8px;border-color:var(--ctl)}:where(html[data-design="kompakt-std"]) .optrow,:where(html[data-design="kompakt-std"]) .ordrow{padding:2px 0}@media(min-width:1100px){html[data-design="kompakt-std"] main>details.card:is(#rcard,#swcard,#updcard,#c_logs,#c_bk,#c_dev,#c_power,#c_layout){grid-column:auto}html[data-design="kompakt-std"] main{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){:where(html[data-design="kompakt-std"]) main{padding:6px 6px 14px;gap:8px}:where(html[data-design="kompakt-std"]) .card{padding:9px 10px}:where(html[data-design="kompakt-std"]) :is(button,input,select){min-height:40px}}html[data-design="kompakt-std"] :is(.ibtn,.helpbtn){width:22px;height:22px;font-size:13px;line-height:20px}html[data-design="kompakt-std"] .swinfo{width:22px;height:22px;font-size:13px;line-height:20px}html[data-design="kompakt-std"] :is(input[type=checkbox],input[type=radio]){width:16px;height:16px;min-height:0;padding:0;accent-color:var(--accent)}html[data-design="kontrast"][data-theme="dark"],html[data-design="kontrast"]:not([data-theme]){--bg:#000000;--card:#0b0b0b;--text:#ffffff;--muted:#e6e6e6;--line:#ffffff;--ctl:#ffffff;--accent:#ffe600;--bar:#ffe600;--blue:#7cc0ff;--ok:#3dff8a;--warn:#ffb300;--crit:#ff6b5e;--onbar:#000000;--onwarn:#000000;--onok:#000000;--oncrit:#000000;--shade:rgba(255,255,255,.18);--focus:#00e5ff;--r:8px;color-scheme:dark}html[data-design="kontrast"][data-theme="light"]{--bg:#ffffff;--card:#ffffff;--text:#000000;--muted:#1a1a1a;--line:#000000;--ctl:#000000;--accent:#0033cc;--bar:#0033cc;--blue:#0033cc;--ok:#005a26;--warn:#7a4200;--crit:#a60014;--onbar:#ffffff;--onwarn:#ffffff;--onok:#ffffff;--oncrit:#ffffff;--shade:rgba(0,0,0,.25);--focus:#d95f00;--r:8px;color-scheme:light}html[data-design="kontrast"] body{font:18px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif}html[data-design="kontrast"] header{border:3px solid var(--line);border-radius:var(--r);padding:10px 14px}html[data-design="kontrast"] h1{font-size:20px;letter-spacing:.02em}html[data-design="kontrast"] main{max-width:1000px;gap:16px;padding:14px 14px 28px}html[data-design="kontrast"] .card{border:3px solid var(--line);border-radius:var(--r);padding:16px}html[data-design="kontrast"] details.card[open]>summary{margin-bottom:12px;padding-bottom:10px;border-bottom:3px solid var(--line)}html[data-design="kontrast"] details.card>summary .sumh,html[data-design="kontrast"] .sech,html[data-design="kontrast"] .subsec>summary,html[data-design="kontrast"] .card h2{font-size:20px;font-weight:800;letter-spacing:.02em}html[data-design="kontrast"] .sech{margin:16px 0 6px}html[data-design="kontrast"] .ph{font-size:16px;line-height:1.55}html[data-design="kontrast"] .big{font-size:34px;font-weight:800}html[data-design="kontrast"] .row{padding:6px 0}html[data-design="kontrast"] :is(input,select,button){min-height:56px;padding:10px 14px;border-radius:var(--r);border:3px solid var(--ctl);font-size:18px}html[data-design="kontrast"] :is(input,select){background:var(--bg);color:var(--text)}html[data-design="kontrast"] button{background:var(--bar);color:var(--onbar);font-weight:800}html[data-design="kontrast"] button.sec,html[data-design="kontrast"] header .hdr button{background:var(--card);color:var(--text);border-color:var(--ctl);font-weight:700}html[data-design="kontrast"] button.warnbtn{background:var(--warn);color:var(--onwarn)}html[data-design="kontrast"] .dangerbtn{background:var(--crit);color:var(--oncrit);border-color:var(--text)}html[data-design="kontrast"] button.mv,html[data-design="kontrast"] button.del{min-height:48px;min-width:48px}html[data-design="kontrast"] .livebtn{font-size:18px;padding:8px 18px;min-height:48px;border-width:3px}html[data-design="kontrast"] .livebtn:not(.on):not(.busy){background:var(--ok);border-color:var(--text);color:var(--onok)}html[data-design="kontrast"] .livebtn.on{background:var(--crit);border-color:var(--text);color:var(--oncrit)}html[data-design="kontrast"] .livebtn.busy{background:var(--warn);border-color:var(--text);color:var(--onwarn)}html[data-design="kontrast"] .seg button{min-height:52px;font-size:17px;border:3px solid var(--ctl);color:var(--text);font-weight:700}html[data-design="kontrast"] .seg button.on{background:var(--bar);color:var(--onbar);border-color:var(--text)}html[data-design="kontrast"] :is(.ibtn,.helpbtn){width:38px;height:38px;min-height:0;padding:0;font-size:20px;line-height:1;border:3px solid var(--ctl);color:var(--text);background:var(--card)}html[data-design="kontrast"] :is(.ibtn,.helpbtn)[aria-expanded=true],html[data-design="kontrast"] #pb_awake[aria-pressed=true]{background:var(--bar);color:var(--onbar);border-color:var(--text)}html[data-design="kontrast"] :is(input[type=checkbox],input[type=radio]){width:30px;height:30px;min-height:0;padding:0;accent-color:var(--accent)}html[data-design="kontrast"] .badge{font-size:15px;font-weight:800;padding:4px 10px;border:2px solid var(--text);border-radius:6px}html[data-design="kontrast"] .badge.attn,html[data-design="kontrast"] .badge.demo,html[data-design="kontrast"] header .hdr>.badge.demo{background:var(--warn);color:var(--onwarn)}html[data-design="kontrast"] .statbox,html[data-design="kontrast"] .camcol,html[data-design="kontrast"] .wstat,html[data-design="kontrast"] .wrow,html[data-design="kontrast"] .chatopts,html[data-design="kontrast"] #sw_notes{border:3px solid var(--line);border-radius:var(--r);padding:12px 14px}html[data-design="kontrast"] .bar{height:16px;border:2px solid var(--text);border-radius:4px;background:var(--bg)}html[data-design="kontrast"] .bar>i{border-radius:0}html[data-design="kontrast"] .alert{border:3px solid var(--text);border-radius:6px;padding:10px 14px;font-size:17px}html[data-design="kontrast"] .hsw.on{background:var(--ok);color:var(--onok)}html[data-design="kontrast"] a{color:var(--accent);text-decoration:underline;text-underline-offset:3px}html[data-design="kontrast"] :is(button,input,select,summary,a):focus-visible{outline:4px solid var(--focus);outline-offset:3px}html[data-design="kontrast"] #mfoot{border:3px solid var(--line);border-radius:14px}html[data-design="kontrast"] .mf-live{min-height:64px;font-size:22px;border:3px solid var(--text);color:var(--onok)}html[data-design="kontrast"] .mf-live.on{color:var(--oncrit)}html[data-design="kontrast"] .mf-live.busy{color:var(--onwarn)}html[data-design="kontrast"] .mf-t{min-height:64px;border:3px solid var(--ctl);font-size:12px;background:var(--card);color:var(--text)}html[data-design="kontrast"] .mf-t svg{width:28px;height:28px}html[data-design="kontrast"] .mf-cam.live{color:var(--ok)}html[data-design="kontrast"] .mf-aud.muted{color:var(--crit);border-color:var(--crit)}html[data-design="kontrast"] .mf-info{font-size:28px;color:var(--text)}html[data-design="cockpit"][data-theme="dark"],html[data-design="cockpit"]:not([data-theme]){--bg:#05080b;--card:#0b1218;--text:#d8f3ff;--muted:#7f9db0;--line:#1c3a4d;--ctl:#2f6a86;--accent:#00e5ff;--bar:#00e5ff;--blue:#4aa8ff;--ok:#27f58a;--warn:#ffb020;--crit:#ff4d4d;--onbar:#021016;--onwarn:#1a1000;--onok:#021a0c;--oncrit:#220404;--shade:rgba(0,229,255,.12);--focus:#ffea00;--r:4px;color-scheme:dark}html[data-design="cockpit"][data-theme="light"]{--bg:#e4eaee;--card:#ffffff;--text:#0b2530;--muted:#486370;--line:#a9bcc7;--ctl:#44687a;--accent:#006c8a;--bar:#007a9c;--blue:#1d5fd1;--ok:#0f7a3d;--warn:#9a5a00;--crit:#b3231f;--onbar:#ffffff;--onwarn:#ffffff;--onok:#ffffff;--oncrit:#ffffff;--shade:rgba(0,40,60,.12);--focus:#b45309;--r:4px;color-scheme:light}html[data-design="cockpit"] body{font:14px/1.35 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif}html[data-design="cockpit"] header{border-radius:var(--r);padding:6px 12px;max-width:min(1400px,calc(100% - 24px));width:calc(100% - 24px);border-color:var(--ctl)}html[data-design="cockpit"] h1{font:700 15px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.14em;text-transform:uppercase}html[data-design="cockpit"] main{max-width:1400px;gap:10px;padding:10px 12px 20px;grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr))}html[data-design="cockpit"] .card{border-radius:var(--r);padding:10px 12px;border:1px solid var(--line);border-left:4px solid var(--accent);background:linear-gradient(180deg,color-mix(in srgb,var(--card) 92%,var(--accent)),var(--card) 40px)}html[data-design="cockpit"] details.card[open]>summary{margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--line)}html[data-design="cockpit"] details.card>summary .sumh,html[data-design="cockpit"] .sech,html[data-design="cockpit"] .subsec>summary,html[data-design="cockpit"] .card h2{font:700 11px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.16em;color:var(--accent)}html[data-design="cockpit"] .big{font:700 40px/1 ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:-.02em;color:var(--accent)}html[data-design="cockpit"] .statbox .big{font-size:38px}html[data-design="cockpit"] .big .unit,html[data-design="cockpit"] .unit{font:400 12px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;color:var(--muted);letter-spacing:.04em}html[data-design="cockpit"] .num,html[data-design="cockpit"] .dyn,html[data-design="cockpit"] .sig .sgv,html[data-design="cockpit"] .row b,html[data-design="cockpit"] .statbox .row span:last-child{font-family:ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;font-variant-numeric:tabular-nums}html[data-design="cockpit"] .row{padding:2px 0}html[data-design="cockpit"] .statgrid{gap:8px;grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr))}html[data-design="cockpit"] .statbox{border:1px solid var(--ctl);border-radius:var(--r);padding:8px 10px 10px;background:color-mix(in srgb,var(--bg) 70%,var(--card));position:relative}html[data-design="cockpit"] .statbox::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);opacity:.7}html[data-design="cockpit"] .ph{font-size:11.5px;line-height:1.4}html[data-design="cockpit"] :is(input,select,button){min-height:34px;padding:5px 9px;border-radius:var(--r)}html[data-design="cockpit"] :is(input,select){border-color:var(--ctl)}html[data-design="cockpit"] button{background:var(--bar);color:var(--onbar);font:700 13px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.04em;text-transform:uppercase}html[data-design="cockpit"] button.sec,html[data-design="cockpit"] header .hdr button{border-color:var(--ctl);text-transform:none}html[data-design="cockpit"] button.warnbtn{color:var(--onwarn)}html[data-design="cockpit"] .dangerbtn{color:var(--oncrit)}html[data-design="cockpit"] .livebtn{font:700 13px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.08em;border-radius:var(--r)}html[data-design="cockpit"] .livebtn:not(.on):not(.busy){background:var(--ok);border-color:var(--ok);color:var(--onok)}html[data-design="cockpit"] .livebtn.busy,html[data-design="cockpit"] .badge.attn,html[data-design="cockpit"] .badge.demo{color:var(--onwarn)}html[data-design="cockpit"] .seg button{min-height:32px;border-radius:var(--r);font:600 12px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;border-color:var(--ctl)}html[data-design="cockpit"] .seg button.on{color:var(--onbar);border-color:var(--bar)}html[data-design="cockpit"] :is(.ibtn,.helpbtn)[aria-expanded=true],html[data-design="cockpit"] #pb_awake[aria-pressed=true],html[data-design="cockpit"] .evchip{color:var(--onbar)}html[data-design="cockpit"] .hsw.on{color:var(--onok)}html[data-design="cockpit"] .badge{border-radius:3px;font:700 11px ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;padding:2px 7px;letter-spacing:.06em}html[data-design="cockpit"] .bar{height:8px;border-radius:0;background:repeating-linear-gradient(90deg,var(--line) 0 5px,transparent 5px 7px)}html[data-design="cockpit"] .bar>i{border-radius:0;-webkit-mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px);mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px)}html[data-design="cockpit"] .dot{box-shadow:0 0 6px currentColor}html[data-design="cockpit"] .dot{color:var(--ok)}html[data-design="cockpit"] .dot.off{color:var(--crit)}html[data-design="cockpit"] .dot.wait{color:var(--warn)}html[data-design="cockpit"] .dot.unk{color:var(--muted);box-shadow:none}html[data-design="cockpit"] .alert{border-radius:3px;padding:6px 10px;font-size:13px}html[data-design="cockpit"] #mfoot{border-radius:6px;border-color:var(--ctl)}html[data-design="cockpit"] .mf-live{border-radius:6px;color:var(--onok);font-family:ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace}html[data-design="cockpit"] .mf-live.busy{color:var(--onwarn)}html[data-design="cockpit"] .mf-t{border-radius:6px;border-color:var(--ctl);font-family:ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace}html[data-design="cockpit"] input[type=checkbox],input[type=radio]{width:16px;height:16px;min-height:0;padding:0;accent-color:var(--accent)}html[data-design="cockpit"] :is(button,input,select,summary,a):focus-visible{outline:3px solid var(--focus);outline-offset:2px}html[data-design="mischpult"][data-theme="dark"],html[data-design="mischpult"]:not([data-theme]){--bg:#16181b;--card:#25282d;--text:#ece7d8;--muted:#a8a396;--line:#0c0d0f;--ctl:#6b7078;--accent:#f2a900;--bar:#f2a900;--blue:#5ca8e8;--ok:#35e06c;--warn:#ffb21e;--crit:#ff3b30;--onbar:#1a1200;--onwarn:#1a1200;--onok:#04200d;--oncrit:#250402;--shade:rgba(0,0,0,.5);--focus:#4dd0ff;--r:4px;color-scheme:dark}html[data-design="mischpult"][data-theme="light"]{--bg:#c4c8cc;--card:#e8eaed;--text:#14161a;--muted:#33373d;--line:#4a4f56;--ctl:#4a4f56;--accent:#8a4b00;--bar:#8a4b00;--blue:#1d5fd1;--ok:#0f7a3d;--warn:#9a5a00;--crit:#b3231f;--onbar:#ffffff;--onwarn:#ffffff;--onok:#ffffff;--oncrit:#ffffff;--shade:rgba(0,0,0,.3);--focus:#0b57d0;--r:4px;color-scheme:light}html[data-design="mischpult"] body{font:14px/1.4 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif}html[data-design="mischpult"] header{border-radius:var(--r);border:1px solid var(--line);background:linear-gradient(180deg,color-mix(in srgb,var(--card) 92%,#fff),var(--card) 55%,color-mix(in srgb,var(--card) 90%,#000));box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 2px 8px var(--shade)}html[data-design="mischpult"] main{gap:14px;padding:14px}html[data-design="mischpult"] .card{position:relative;border-radius:var(--r);border:1px solid var(--line);padding:14px 16px 14px;background:linear-gradient(180deg,color-mix(in srgb,var(--card) 92%,#fff),var(--card) 55%,color-mix(in srgb,var(--card) 90%,#000));box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 -2px 0 rgba(0,0,0,.25),0 3px 8px var(--shade)}html[data-design="mischpult"] details.card::before{content:"";position:absolute;width:7px;height:7px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#b9bdc3,#4a4d52 70%);box-shadow:0 0 0 1px rgba(0,0,0,.55),inset 0 -1px 1px rgba(0,0,0,.4);pointer-events:none;top:5px;left:5px}html[data-design="mischpult"] details.card::after{content:"";position:absolute;width:7px;height:7px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#b9bdc3,#4a4d52 70%);box-shadow:0 0 0 1px rgba(0,0,0,.55),inset 0 -1px 1px rgba(0,0,0,.4);pointer-events:none;top:5px;right:5px}html[data-design="mischpult"] details.card>summary{padding:0 14px}html[data-design="mischpult"] details.card>summary .sumh,html[data-design="mischpult"] .sech,html[data-design="mischpult"] .subsec>summary,html[data-design="mischpult"] .card h2{color:var(--text);font-weight:800;letter-spacing:.14em;text-shadow:0 1px 0 rgba(255,255,255,.15)}html[data-design="mischpult"] details.card[open]>summary{margin-bottom:10px;padding-bottom:8px;border-bottom:2px groove var(--line)}html[data-design="mischpult"] .sech{color:var(--accent)}html[data-design="mischpult"] .big{font-size:28px;font-weight:700;color:var(--text);text-shadow:0 0 10px color-mix(in srgb,var(--accent) 35%,transparent);font-variant-numeric:tabular-nums}html[data-design="mischpult"] .statbox{border:1px solid var(--line);border-radius:3px;background:color-mix(in srgb,var(--bg) 55%,var(--card));box-shadow:inset 0 2px 5px rgba(0,0,0,.45)}html[data-design="mischpult"] :is(input,select){background:color-mix(in srgb,var(--bg) 70%,#000);border:1px solid var(--line);border-radius:3px;box-shadow:inset 0 2px 4px rgba(0,0,0,.55);color:var(--text)}html[data-design="mischpult"] :is(input,select,button){border-radius:3px}html[data-design="mischpult"] button{background:linear-gradient(180deg,color-mix(in srgb,var(--bar) 88%,#fff),var(--bar) 55%,color-mix(in srgb,var(--bar) 80%,#000));color:var(--onbar);font-weight:800;letter-spacing:.04em;border:1px solid var(--line);box-shadow:0 3px 0 var(--line),inset 0 1px 0 rgba(255,255,255,.35)}html[data-design="mischpult"] button.sec,html[data-design="mischpult"] header .hdr button,html[data-design="mischpult"] .seg button,html[data-design="mischpult"] :is(.ibtn,.helpbtn){background:linear-gradient(180deg,color-mix(in srgb,var(--card) 80%,#fff),color-mix(in srgb,var(--card) 85%,#000));color:var(--text);font-weight:600;border:1px solid var(--line);box-shadow:0 3px 0 var(--line),inset 0 1px 0 rgba(255,255,255,.18)}html[data-design="mischpult"] button:active{transform:translateY(2px);box-shadow:0 1px 0 var(--line),inset 0 1px 3px rgba(0,0,0,.4)}html[data-design="mischpult"] button:disabled{box-shadow:none;transform:none}html[data-design="mischpult"] button.warnbtn{background:linear-gradient(180deg,color-mix(in srgb,var(--warn) 85%,#fff),var(--warn));color:var(--onwarn)}html[data-design="mischpult"] .dangerbtn{background:linear-gradient(180deg,color-mix(in srgb,var(--crit) 85%,#fff),var(--crit));color:var(--oncrit)}html[data-design="mischpult"] .livebtn{border-radius:3px;letter-spacing:.1em;text-transform:uppercase}html[data-design="mischpult"] .livebtn:not(.on):not(.busy){background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 85%,#fff),var(--ok));color:var(--onok);box-shadow:0 3px 0 var(--line),0 0 12px color-mix(in srgb,var(--ok) 60%,transparent)}html[data-design="mischpult"] .livebtn.on{background:linear-gradient(180deg,color-mix(in srgb,var(--crit) 85%,#fff),var(--crit));color:var(--oncrit);box-shadow:0 3px 0 var(--line),0 0 12px color-mix(in srgb,var(--crit) 60%,transparent)}html[data-design="mischpult"] .livebtn.busy,html[data-design="mischpult"] .badge.attn,html[data-design="mischpult"] .badge.demo{color:var(--onwarn)}html[data-design="mischpult"] .seg button.on,html[data-design="mischpult"] :is(.ibtn,.helpbtn)[aria-expanded=true],html[data-design="mischpult"] #pb_awake[aria-pressed=true]{background:linear-gradient(180deg,color-mix(in srgb,var(--bar) 88%,#fff),var(--bar));color:var(--onbar);box-shadow:inset 0 2px 4px rgba(0,0,0,.45)}html[data-design="mischpult"] .hsw.on{color:var(--onok)}html[data-design="mischpult"] .badge{border-radius:3px;font-weight:700;letter-spacing:.06em;border:1px solid var(--line)}html[data-design="mischpult"] .bar{height:14px;padding:2px;border-radius:3px;background:color-mix(in srgb,var(--bg) 55%,#000);border:1px solid var(--line);box-shadow:inset 0 2px 4px rgba(0,0,0,.6)}html[data-design="mischpult"] .bar>i{border-radius:1px;-webkit-mask:repeating-linear-gradient(90deg,#000 0 6px,transparent 6px 8px);mask:repeating-linear-gradient(90deg,#000 0 6px,transparent 6px 8px);box-shadow:0 0 8px color-mix(in srgb,var(--bar) 60%,transparent)}html[data-design="mischpult"] .dot{width:12px;height:12px;border:1px solid var(--line);box-shadow:0 0 8px 1px var(--ok),inset 0 -2px 3px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.5)}html[data-design="mischpult"] .dot.off{box-shadow:0 0 8px 1px var(--crit),inset 0 -2px 3px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.5)}html[data-design="mischpult"] .dot.wait{box-shadow:0 0 8px 1px var(--warn),inset 0 -2px 3px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.5)}html[data-design="mischpult"] .dot.unk{box-shadow:inset 0 -2px 3px rgba(0,0,0,.35)}html[data-design="mischpult"] .alert{border-radius:3px;box-shadow:inset 0 2px 4px rgba(0,0,0,.35)}html[data-design="mischpult"] #mfoot{border-radius:6px;border:1px solid var(--line);background:linear-gradient(180deg,color-mix(in srgb,var(--card) 92%,#fff),var(--card) 55%,color-mix(in srgb,var(--card) 90%,#000));box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 0 0 8px var(--bg),0 -12px 12px -4px var(--shade)}html[data-design="mischpult"] .mf-live{border-radius:4px;color:var(--onok);box-shadow:0 3px 0 var(--line),0 0 12px color-mix(in srgb,var(--ok) 50%,transparent)}html[data-design="mischpult"] .mf-live.busy{color:var(--onwarn)}html[data-design="mischpult"] .mf-t{border-radius:4px;border:1px solid var(--line);background:linear-gradient(180deg,color-mix(in srgb,var(--card) 80%,#fff),color-mix(in srgb,var(--card) 85%,#000));box-shadow:0 3px 0 var(--line),inset 0 1px 0 rgba(255,255,255,.18)}html[data-design="mischpult"] :is(button,input,select,summary,a):focus-visible{outline:3px solid var(--focus);outline-offset:2px}html[data-design="cockpit"] .mf-t{min-height:60px;gap:3px;padding:3px;font:700 12px/1.1 ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.02em;color:var(--text);background:color-mix(in srgb,var(--bg) 55%,var(--card));border:2px solid var(--ctl)}html[data-design="cockpit"] .mf-t svg{width:26px;height:26px}html[data-design="cockpit"] .mf-cam.live{color:var(--ok);border-color:var(--ok)}html[data-design="cockpit"] .mf-cam.ismain{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent);color:var(--accent)}html[data-design="cockpit"] .mf-cam.ismain.live{color:var(--ok);border-color:var(--ok);box-shadow:inset 0 0 0 1px var(--ok)}html[data-design="cockpit"] .mf-cam.off{color:var(--muted)}html[data-design="cockpit"] .mf-aud{color:var(--text)}html[data-design="cockpit"] .mf-aud.muted{color:var(--crit);border-color:var(--crit)}html[data-design="cockpit"] .mf-live{min-height:60px;font:800 20px/1 ui-monospace,"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",monospace;letter-spacing:.06em;text-transform:uppercase}html[data-design="cockpit"] .mf-info{font-size:26px;color:var(--text)}html[data-design="mischpult"] .mf-t{min-height:60px;gap:3px;padding:3px;font:700 12px/1.1 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif;letter-spacing:.03em;color:var(--text);background:linear-gradient(180deg,color-mix(in srgb,var(--bg) 70%,#000),color-mix(in srgb,var(--bg) 85%,var(--card)));border:1px solid var(--line);box-shadow:0 3px 0 var(--line),inset 0 1px 0 rgba(255,255,255,.12)}html[data-design="mischpult"] .mf-t svg{width:26px;height:26px}html[data-design="mischpult"] .mf-t:active{transform:translateY(2px);box-shadow:0 1px 0 var(--line)}html[data-design="mischpult"] .mf-cam.live{color:var(--ok);box-shadow:0 3px 0 var(--line),inset 0 -3px 0 var(--ok)}html[data-design="mischpult"] .mf-cam.ismain{border-color:var(--accent);box-shadow:0 3px 0 var(--line),inset 0 0 0 1px var(--accent)}html[data-design="mischpult"] .mf-cam.ismain.live{box-shadow:0 3px 0 var(--line),inset 0 0 0 1px var(--accent),inset 0 -3px 0 var(--ok)}html[data-design="mischpult"] .mf-cam.off{color:var(--muted)}html[data-design="mischpult"] .mf-aud{color:var(--text)}html[data-design="mischpult"] .mf-aud.muted{color:var(--crit);border-color:var(--crit)}html[data-design="mischpult"] .mf-live{min-height:60px;font-size:20px;font-weight:800}html[data-design="mischpult"] .mf-info{font-size:26px;color:var(--text)}html[data-design="mischpult"][data-theme="light"] :is(input,select){background:#f8f9fb;color:#14161a;border:1px solid #4a4f56;box-shadow:inset 0 1px 3px rgba(0,0,0,.25)}html[data-design="mischpult"][data-theme="light"] details.card>summary .sumh,html[data-design="mischpult"][data-theme="light"] .sech,html[data-design="mischpult"][data-theme="light"] .subsec>summary,html[data-design="mischpult"][data-theme="light"] .card h2{text-shadow:none}html[data-design="mischpult"][data-theme="light"] .sech{color:#7a4200}html[data-design="mischpult"][data-theme="light"] .big{text-shadow:none;color:#14161a}html[data-design="mischpult"][data-theme="light"] .badge{background:#d3d6db;color:#14161a;border:1px solid #4a4f56}html[data-design="mischpult"][data-theme="light"] .badge.attn,html[data-design="mischpult"][data-theme="light"] .badge.demo{background:#e69a00;color:#14161a}html[data-design="mischpult"][data-theme="light"] button{background:linear-gradient(180deg,#a65a00,#7d4300);color:#ffffff;border:1px solid #2b2d31;box-shadow:0 3px 0 #2b2d31,inset 0 1px 0 rgba(255,255,255,.3);text-shadow:none}html[data-design="mischpult"][data-theme="light"] button.sec,html[data-design="mischpult"][data-theme="light"] header .hdr button,html[data-design="mischpult"][data-theme="light"] .seg button,html[data-design="mischpult"][data-theme="light"] :is(.ibtn,.helpbtn){background:linear-gradient(180deg,#fafbfc,#d3d6db);color:#14161a;border:1px solid #4a4f56;box-shadow:0 3px 0 #4a4f56,inset 0 1px 0 #fff}html[data-design="mischpult"][data-theme="light"] .seg button.on,html[data-design="mischpult"][data-theme="light"] :is(.ibtn,.helpbtn)[aria-expanded=true],html[data-design="mischpult"][data-theme="light"] #pb_awake[aria-pressed=true]{background:linear-gradient(180deg,#a65a00,#7d4300);color:#ffffff;box-shadow:inset 0 2px 4px rgba(0,0,0,.45)}html[data-design="mischpult"][data-theme="light"] button.warnbtn{background:linear-gradient(180deg,#e69a00,#b87800);color:#14161a}html[data-design="mischpult"][data-theme="light"] .dangerbtn{background:linear-gradient(180deg,#d6453d,#a82a25);color:#ffffff}html[data-design="mischpult"][data-theme="light"] .livebtn:not(.on):not(.busy),html[data-design="mischpult"][data-theme="light"] .mf-live{background:linear-gradient(180deg,#1f9d4a,#0f7a3d);color:#ffffff;box-shadow:0 3px 0 #2b2d31,0 0 10px rgba(15,122,61,.45)}html[data-design="mischpult"][data-theme="light"] .livebtn.on{background:linear-gradient(180deg,#d6453d,#a82a25);color:#ffffff}html[data-design="mischpult"][data-theme="light"] .livebtn.busy{background:linear-gradient(180deg,#e69a00,#b87800);color:#14161a}html[data-design="mischpult"][data-theme="light"] .mf-t{background:linear-gradient(180deg,#fafbfc,#d3d6db);color:#14161a;border:1px solid #4a4f56;box-shadow:0 3px 0 #4a4f56,inset 0 1px 0 #fff}html[data-design="mischpult"][data-theme="light"] .mf-cam.live{color:#0a6b30;box-shadow:0 3px 0 #4a4f56,inset 0 -3px 0 #0f7a3d}html[data-design="mischpult"][data-theme="light"] .mf-cam.ismain{border-color:#8a4b00;box-shadow:0 3px 0 #4a4f56,inset 0 0 0 1px #8a4b00}html[data-design="mischpult"][data-theme="light"] .mf-cam.off{color:#4a4f56}html[data-design="mischpult"][data-theme="light"] .mf-aud.muted{color:#a82a25;border-color:#a82a25}html[data-design="mischpult"][data-theme="light"] .bar{background:#4a4f56;border:1px solid #14161a}html[data-design="mischpult"][data-theme="light"] .statbox{background:#dfe2e6;border:1px solid #4a4f56}html[data-design="mischpult"][data-theme="light"] .alert{background:#f4e3b5;color:#14161a}html[data-design] header h1{white-space:nowrap}html[data-design] header .hdr{gap:6px}html[data-design] :is(.pbpick,.langbtns) button{background:var(--bg);color:var(--text);border:1px solid var(--ctl,var(--line));font-weight:500;border-radius:10px;min-height:44px;padding:10px 12px;font-size:16px;filter:none}html[data-design] :is(.pbpick,.langbtns) button.on{background:var(--bar);border-color:var(--bar);color:var(--onbar,var(--card));font-weight:700}</style> <link rel="stylesheet" href="/chat.css"> </head> <body> <header><h1 class="hl" aria-label="IRL4YOU BOX"><span class="hlogo"><svg viewBox="0 0 1024 1024" width="26" height="26" aria-hidden="true" focusable="false"><defs><mask id="hl4m"><rect width="1024" height="1024" fill="#fff"/><circle cx="757" cy="267" r="102" fill="#000"/></mask></defs><circle cx="512" cy="512" r="472" fill="#12213c"/><circle cx="512" cy="512" r="448" fill="none" stroke="#1cdaf5" stroke-width="48" mask="url(#hl4m)"/><path d="M599 252 295 612H715M599 252V757" fill="none" stroke="#f8fafc" stroke-width="100" stroke-linecap="round" stroke-linejoin="round"/><circle cx="757" cy="267" r="72" fill="#ffa14f"/></svg></span><span class="htxt">IRL4YOU BOX</span></h1><span class="hdr"><span id="hdr_sig"></span><button type="button" id="hdr_upd" class="updbtn" hidden aria-label="Neue Version verfügbar, zum Software-Update" title="Neue Version verfügbar"><span class="dot wait"></span><span class="u1">Update </span>Oberfläche</button><button type="button" id="hdr_sys" class="updbtn" hidden aria-label="Systemupdates verfügbar, zu den Systemupdates" title="Systemupdates verfügbar"><span class="dot wait"></span><span class="u1">Update </span>System</button> <span class="foldd"><button type="button" class="sec small" id="fold_all">Alle zuklappen</button><button type="button" class="sec small" id="unfold_all">Alle aufklappen</button></span> <button type="button" class="sec small foldm" id="fold_toggle" title="Alle Karten zu- oder aufklappen" aria-label="Alle Karten zu- oder aufklappen">⇕</button> <select id="lang_sel" data-lang translate="no" class="sec small" aria-label="Language" title="Language"></select> <button type="button" class="sec small" id="pb_awake" aria-pressed="false" title="Bildschirm anlassen" aria-label="Bildschirm anlassen">💡</button> <button type="button" class="sec small" id="theme_btn" aria-label="Hell oder dunkel" title="Hell oder dunkel"></button> <button type="button" class="sec small" id="sm_btn" title="Streammodus: blendet Adressen, Namen und Protokolle aus, z. B. wenn der Bildschirm gefilmt wird" aria-label="Streammodus"><span class="smic">👁</span><span class="smtx">Streammodus: aus</span></button> <button type="button" id="hdr_live" class="livebtn" hidden aria-label="Sendung starten oder beenden">Live</button><button type="button" id="hdr_more" class="sec small" aria-expanded="false" aria-label="Weitere Knöpfe ein- oder ausklappen" title="Weitere Knöpfe ein- oder ausklappen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg></button></span></header> <main> <section class="card wide" id="livecard" aria-label="Live"> <div id="livebox"> <div class="row liverow" style="justify-content:flex-start"><span class="big" id="livestate">–</span><span id="liveup" class="liveup" hidden title="Gesamter Upload über die gewählten Netze"></span><span id="liveserver" class="muted livesrv"></span><span id="livetemp" class="livetemp" hidden title="Temperatur der Box (Prozessor)"></span></div> <div id="swapbar" hidden><div class="sech" style="margin:12px 0 6px">Hauptbild</div><div class="seg" id="swapbtns" role="group" aria-label="Hauptbild wählen"></div></div> <div id="livewhy" class="ph"></div> <div id="livehint" class="pending" hidden><span>Live geht erst, wenn der Bildaufbau fertig ist: Hauptkamera wählen und „Bildaufbau speichern“.</span><button type="button" id="live_topipe" class="warnbtn">Zum Bildaufbau</button></div> <div id="srvhint" class="pending" hidden><span>Live geht erst, wenn ein SRTLA-Server eingetragen ist: Server hinzufügen und auswählen.</span><button type="button" id="live_tosrv" class="warnbtn">Zum SRTLA-Server</button></div> <div id="nethint" class="pending" hidden><span>Zwei Verbindungen nutzen denselben Adressbereich bei verschiedenen Geräten (zum Beispiel zwei Modems oder Hotspots mit 192.168.8.x): Das führt zu Adresskonflikten, und Kameras können ausfallen. Bitte eine davon trennen:</span> <b id="nethint_cards"></b></div> <div id="livepending" class="pending" hidden><span id="livepending_t"></span> <button type="button" id="live_restart" class="warnbtn">Stream neu starten</button><button type="button" id="live_ignore" class="sec">Später</button></div> <div id="liveerr" class="err"></div> <div id="livehealth" class="livehealth"></div> </div> </section> <details class="card wide" id="c_prev"><summary><span class="sumh">Vorschau</span><span class="prevctl"><button type="button" id="prev_set" class="sec small prevset" hidden aria-label="Einstellung zeigen" aria-expanded="false">⚙</button><button type="button" id="prev_btn" class="sec small">Start</button><button type="button" class="helpbtn" id="prev_help_btn" aria-label="Erklärung zeigen" aria-expanded="false">i</button></span></summary> <div id="prev_help" class="ph" hidden>Zeigt das fertig gemischte Bild so, wie es gerade gesendet wird, in kleiner Auflösung: also auch, wohin die Kameras gerade zeigen und wie die Bilder angeordnet sind. Es sind 30 Bilder pro Sekunde in 640 × 360 als Video, rund 1,7 Mbit/s zum Browser, über das Netz, in dem er angemeldet ist. Die Vorschau endet nach 10 Minuten, beim Zuklappen der Karte, beim Schließen der Seite und wenn die Sendung endet. Sie kostet die Box etwa die Hälfte eines der vier kleinen Prozessorkerne, solange sie läuft; die Sendung selbst bleibt unberührt. Mit dem Häkchen darunter entfällt die Grenze von 10 Minuten: Die Vorschau startet dann in diesem Browser von selbst wieder, sobald die Seite sichtbar ist und die Box sendet.</div> <label class="prevkeep"><input type="checkbox" id="prev_always"> Mir ist bewusst, dass die Vorschau Rechenleistung kostet. Ich möchte sie in diesem Browser dauerhaft eingeschaltet lassen.</label> <div id="pa_box"> <div class="autolbl" id="pa_lbl"><label class="chk" id="pa_row"><input type="checkbox" id="pa_ext"> Vorschau auch von außen erlauben (verbraucht Upload)</label> <button type="button" class="helpbtn" id="pa_help_btn" aria-label="Erklärung zeigen" aria-expanded="false">i</button></div> <div class="ph" id="pa_help" hidden>Jede Vorschau braucht rund 1,7 Mbit/s Upload, den die Sendung braucht. Von außen (Tailscale, öffentlicher Link) ist die Vorschau deshalb aus. Mit dem Haken geht sie von außen nur klein (320 Pixel, 10 Bilder pro Sekunde, etwa 0,3 Mbit/s) und für einen Zuschauer zugleich. Im Heimnetz und im WLAN der Box bleibt alles wie es ist.</div> <div id="pa_err" class="err"></div> </div> <div id="prev_msg" class="ph" role="status"></div> <div class="actions" style="margin:6px 0 0"><button type="button" id="prev_live" class="livebtn" hidden>Live gehen</button></div> <div id="prev_box" hidden><video id="prev_vid" muted playsinline autoplay hidden aria-label="Vorschau des Sendebilds"></video><img id="prev_img" alt="Vorschau des Sendebilds" hidden></div> </details> <details class="card wide" id="c_chat" open><summary><span class="sumh">Chat</span><span id="net_sig" class="sig" role="img" aria-label="Verbindung: keine Messung" title="Verbindung: keine Messung"><i class="sigdot"></i><span class="sgs" aria-hidden="true"><span id="sg_max">10</span><span id="sg_mid">5,5</span><span id="sg_min">1</span></span><svg viewBox="0 0 80 20" preserveAspectRatio="none" aria-hidden="true"><path class="sg" d="M0 1H80M0 10H80M0 19H80"></path><polyline id="net_line" points=""></polyline></svg><span id="sg_val" class="sgv"></span></span><span id="cam_dots" class="camdots" role="img"></span><button type="button" id="chat_opt" class="sec small chatoptbtn" aria-expanded="false" title="Konto und Ansicht des Chats" aria-label="Konto und Ansicht des Chats">⚙</button></summary> <div class="twacc twch" id="tw_chrow" hidden><label class="muted" for="tw_ch_in">Kanal (Konto, auf dem gestreamt wird)</label><input id="tw_ch_in" maxlength="25" autocomplete="off" autocapitalize="off" spellcheck="false"><button type="button" id="tw_ch_save" class="sec small">Speichern</button><span id="tw_ch_t" class="muted"></span></div> <div class="twacc" id="tw_accrow"><span id="tw_acc_t" class="muted"></span><button type="button" id="tw_acc_go" class="sec small">Mit Twitch anmelden</button><button type="button" id="tw_acc_asmod" class="sec small" hidden>Als Moderator anmelden</button><button type="button" id="tw_acc_owner" class="sec small" hidden>Ich bin der Streamer</button></div> <div id="chat_optbox" class="chatopts" hidden> <div class="accrow twch" id="tw_chrow2"><label class="muted" for="tw_ch2_in">Kanal (Konto, auf dem gestreamt wird)</label><input id="tw_ch2_in" maxlength="25" autocomplete="off" autocapitalize="off" spellcheck="false"><button type="button" id="tw_ch2_save" class="sec small">Speichern</button></div> <div class="accrow" id="tw_accpanel" hidden><span id="tw_acc_t2" class="muted" translate="no"></span><span id="tw_acc_ev_on" class="muted" hidden>Ereignisse an</span><button type="button" id="tw_acc_mod" class="sec small" hidden>Moderation einschalten</button><button type="button" id="tw_acc_modoff" class="sec small" hidden>Moderation ausschalten</button><button type="button" id="tw_acc_ev" class="sec small" hidden>Ereignisse einschalten</button><button type="button" id="tw_acc_out" class="sec small">Abmelden</button></div> <label><input type="checkbox" data-opt="ts" checked> Uhrzeit</label> <label><input type="checkbox" data-opt="badge" checked> Abzeichen</label> <label><input type="checkbox" data-opt="emote" checked> Emotes als Bild</label> <label><input type="checkbox" data-opt="line" checked> Trennlinien</label> <label><input type="checkbox" data-opt="sig"> Signal bei Ereignissen</label> <label>Schrift <select data-opt="size"><option value="s">klein</option><option value="m" selected>normal</option><option value="l">groß</option></select></label> </div> <div id="tw_acc_code" class="twcode" hidden> <div id="tw_qr"></div> <div class="twcode_txt"><div class="twcode_big" id="tw_code" translate="no"></div><a id="tw_link" href="#" target="_blank" rel="noopener">twitch.tv/activate</a> <div><button type="button" id="tw_acc_cancel" class="sec small">Abbrechen</button></div></div> </div> <div id="chat_msg" class="muted" hidden></div> <div class="chatwrap"><div id="chat_box" class="chatbox" role="log" aria-live="polite" tabindex="0"></div><button type="button" id="chat_evchip" class="evchip" hidden>Neues Ereignis ↓</button></div> <form id="chat_form" class="chatform" hidden autocomplete="off"><input id="chat_in" maxlength="450" placeholder="Nachricht schreiben" aria-label="Nachricht schreiben"><button type="submit" id="chat_send">Senden</button></form> <div id="chat_err" class="err"></div> <div id="chat_toast" class="chattoast" role="status" hidden><span id="chat_toast_t"></span> <button type="button" id="chat_undo" class="sec small" hidden>Rückgängig</button></div> </details> <details class="card wide" id="c_deck" open><summary><span class="sumh">Streamerbot Deck</span><button type="button" id="deck_opt" class="sec small chatoptbtn" aria-expanded="false" title="Adresse des Decks" aria-label="Adresse des Decks">⚙</button></summary> <div id="deck_optbox" class="chatopts" hidden> <div class="accrow"><label class="muted" for="deck_in">Adresse des Streamer.bot-Decks</label><input id="deck_in" type="url" maxlength="500" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="https://streamer.bot/decks/…" style="max-width:none;flex:1 1 260px"></div> <div class="accrow"><label class="muted" for="deck_w">Größe für Desktop und Tablet in Pixel (Breite × Höhe)</label><input id="deck_w" type="number" inputmode="numeric" min="200" max="4000" step="10" style="max-width:90px"> × <input id="deck_h" type="number" inputmode="numeric" min="320" max="4000" step="10" style="max-width:90px"></div> <div class="accrow"><label class="muted" for="deck_mw">Größe für Mobil in Pixel (Breite × Höhe)</label><input id="deck_mw" type="number" inputmode="numeric" min="200" max="4000" step="10" style="max-width:90px"> × <input id="deck_mh" type="number" inputmode="numeric" min="320" max="4000" step="10" style="max-width:90px"><button type="button" id="deck_save" class="sec small">Speichern</button></div> <div class="muted">Adresse und Größen sind auf der Box gespeichert und gelten für alle Geräte. Das Deck wird in der Größe der jeweiligen Ansicht gezeigt und auf schmalen Bildschirmen passend verkleinert. Die Höhe muss für das Auswahlfeld der Seiten reichen (mindestens 320). Leere Adresse speichern entfernt das Deck.</div> </div> <div id="deck_msg" class="muted" hidden></div> <div id="deck_wrap" class="chatwrap deckwrap"></div> </details> <details class="card wide" id="c_status" open><summary><span class="sumh">Status</span></summary> <div id="alerts"></div> <div class="statgrid"> <div class="statbox"><div class="sech">Up- und Download</div><div id="net"></div></div> <div class="statbox"><div id="camlights"><div class="sech">Kameras</div></div></div> <div class="statbox"><div class="sech">System</div> <div class="row" style="padding-bottom:0"><span class="muted">CPU</span><span class="muted"><span id="cpumax"></span> · <span id="temp">–</span> °C</span></div> <div class="row" style="padding-top:0"><span class="big"><span id="cpu">–</span><span class="unit"> %</span></span></div> <div class="bar"><i id="cpubar" style="width:0"></i></div> <div class="muted" id="fanline" hidden title="Ansteuerung des Lüfters, keine gemessene Drehzahl (die Box hat keinen Drehzahlanschluss am Lüfter)">Lüfter: <span id="fan">–</span> % (Ansteuerung)</div> <div class="row" style="margin-top:4px;padding-bottom:0"><span class="muted">Arbeitsspeicher</span><span class="muted"><span id="memfree">–</span> frei</span></div> <div class="row" style="padding-top:0"><span class="big"><span id="mem">–</span><span class="unit"> % belegt</span></span></div> <div class="bar"><i id="membar" style="width:0"></i></div> </div> </div> <details class="dsec" id="statdet"><summary>Details</summary> <div class="statgrid"> <div class="statbox"><div class="sech">Senden</div><div id="det_send"></div></div> <div class="statbox"><div class="sech">Prozessor</div><div id="det_cpu"></div></div> <div class="statbox"><div class="sech">Temperaturen</div><div id="det_temp"></div></div> <div class="statbox"><div class="sech">Speicherplatz</div><div id="det_disk"></div></div> </div> </details> </details> <details class="card wide" id="netcard" open><summary><span class="sumh">Verbindungen</span></summary> <details class="subsec" id="net_up"><summary>Verbindungen zum Senden (Upload)</summary> <div class="ph" style="margin-top:0">Die Box sendet gleichzeitig über alle Netze mit Häkchen (Bonding). Das Häkchen wird sofort gespeichert und wirkt auch während der Sendung nach wenigen Sekunden. Das Kameranetz darf mitgenutzt werden.</div> <div id="b_ups"></div> <div id="neterr" class="err"></div> <div id="upinfo" class="ph"></div> </details> <details class="subsec" id="net_wifi"><summary>WLAN-Verbindungen (z. B. Handy-Hotspot, auch zum Senden nutzbar)</summary><div class="sens"><div id="wificard"> <div class="ph" style="margin-top:0">Verbindet die Box mit einem WLAN, zum Beispiel dem Hotspot eines Handys. Danach in der Zeile der WLAN-Karte „Zum Senden verwenden“ ankreuzen (wird sofort gespeichert). Das Passwort wird nur an das Netzwerk-Programm der Box übergeben und nicht gespeichert.</div> <div id="wifidev"></div> <div id="hsmsg" class="ph" style="margin:6px 0" role="status"></div> <div class="wform"> <label class="muted">WLAN-Karte<select id="w_if"></select></label> <label class="muted">Netzname (SSID)<input id="w_ssid" maxlength="32" autocomplete="off"></label> <label class="muted">Passwort<input id="w_pw" type="password" maxlength="64" autocomplete="new-password" placeholder="leer bei offenem Netz"></label> </div> <label class="muted" style="display:block;margin:8px 0"><input type="checkbox" id="w_hidden"> verstecktes Netz (Name wird nicht ausgesendet)</label> <div class="actions"><button id="w_scan" type="button" class="sec">Netze suchen</button> <button id="w_connect" type="button">Verbinden</button> <button id="w_disc" type="button" class="sec">Trennen</button></div> <div id="wifidriver" class="ph" style="margin:8px 0" role="status"></div> <div id="wifimsg" class="ph" style="margin:8px 0" role="status"></div> <div id="wifinets" class="ph"></div> <div id="wifisaved" class="ph"></div> </div></div> </details> <details class="subsec" id="net_bt"><summary>Bluetooth</summary> <div class="ph" style="margin-top:0">Die Bluetooth-Sticks dieser Box. Steht ein Stick als nicht nutzbar da, fehlt ihm der Treiber oder er wird nicht unterstützt.</div> <div id="btcard"><div class="ph">Wird geladen …</div></div> <div id="btdrvprog" class="prog" hidden><div class="bar"><i></i></div><div class="progline"><span><b class="progpct"></b> <span class="progtxt"></span></span><span class="progel muted"></span></div></div> <div id="btdrvmsg" class="ph" role="status"></div> <div class="actions"><button id="btdrv_go" type="button" disabled>Treiber jetzt einrichten</button></div> <div class="ph" data-hlp>Baut und lädt den Bluetooth-Treiber für Sticks, die der Kernel nicht kennt (zum Beispiel TP-Link UB500 oder UGREEN BT6.0). Das dauert einige Minuten, Bluetooth fehlt dabei kurz. Während einer Übertragung wartet die Box damit bis zum Ende. Auf der ROCK 5B Plus richtet der Knopf außerdem den Bluetooth-Chip des Boards ein, wenn er noch nicht läuft.</div> <div class="sech">Handyverbindungen</div> <div class="ph" data-hlp>Zeigt den Akkustand gekoppelter Handys. Die Box fragt alle 10 Minuten kurz nach und trennt danach wieder, es gibt keine Dauerverbindung. Android meldet den Akku in Schritten von 20 %, ein iPhone in Schritten von etwa 10 %. Das Handy muss in der Nähe sein und Bluetooth an haben.</div> <div id="phonecard"></div> <div class="actions"><button id="ph_pair" type="button">Handy koppeln</button></div> <div id="ph_msg" class="ph"></div> <div class="sech">Controller</div> <div class="ph" data-hlp>Bluetooth-Controller mit Tasten, zum Beispiel ein Mini-Tastenfeld oder ein Gamepad. Controller einschalten, in den Kopplungsmodus bringen, nah an die Box legen und suchen. Funktionen für die Tasten folgen später.</div> <div id="ctrlcard"></div> <div class="actions"><button id="ct_scan" type="button">Nach Controllern suchen</button> <label class="chk"><input type="checkbox" id="ct_all"> Alle Geräte zeigen</label></div> <div id="ctrlfound"></div> <div id="ct_msg" class="ph"></div> </details> <details class="subsec" id="net_ua"><summary>Zugriff auf diese Oberfläche</summary> <label class="chk"><input type="checkbox" id="ua_block"> Über fremde WLANs sperren</label> <div class="ph" data-hlp>Die Oberfläche ist unverschlüsselt (HTTP). In einem WLAN, in dem die Box nur Gast ist (Hotel, Handy-Hotspot, fremdes Netz), kann dort jeder mitlesen. Ist der Schalter an, antwortet die Box in solchen WLANs nicht. Erreichbar bleiben Ethernet, der Hotspot der Box, USB und Tailscale (HTTPS). Wer sich aussperrt, schaltet den Schalter über Ethernet oder Tailscale wieder aus.</div> <div id="ua_info" class="ph"></div> <div id="ua_err" class="err"></div> </details> </details> <details class="card wide" id="srtlacard" open><summary><span class="sumh">SRTLA: Server, Bitrate und Latenz</span><span class="lockhint" id="srtlalock" hidden> · Streammodus ist an</span></summary> <div class="autolbl"><label><input type="checkbox" id="auto_on"> Beim Start der Box automatisch live gehen</label> <button type="button" class="helpbtn" id="auto_help_btn" aria-label="Erklärung zeigen" aria-expanded="false">i</button></div> <div id="auto_help" class="ph" hidden>Gilt nur nach dem Hochfahren der Box, einmal pro Start: Sobald eine Kamera sendet (die Box wartet bis zu 10 Minuten darauf), geht sie von selbst live. „Live beenden“ bricht die Automatik für diesen Start ab. Verbraucht Mobilfunkdaten.</div> <div id="autostatus" class="ph" hidden></div> <details class="subsec" id="srt_srv" open><summary>SRTLA-Server</summary> <div class="ph" style="margin-top:0">SRTLA-Server speichern und per Auswahl umschalten. Die Stream-ID ist ein Zugangsschlüssel: sie wird auf der Box gespeichert und hier nie wieder angezeigt.</div> <div class="srvrow"><select id="srv_sel"></select><button id="srv_edit" type="button" class="sec">Bearbeiten</button><button id="srv_del" type="button" class="sec">Löschen</button></div> <form id="srvform" class="add" style="margin-top:10px"> <input id="s_name" placeholder="Name (z. B. Mein Server)" maxlength="40" required> <input id="s_host" placeholder="Adresse (z. B. srtla.example.de)" maxlength="253" required autocomplete="off"> <input id="s_port" placeholder="Port" type="number" min="1" max="65535" required> <input id="s_sid" type="password" placeholder="Stream-ID (optional)" maxlength="512" autocomplete="new-password"> <button type="submit" id="srv_save">Server hinzufügen</button> <button type="button" id="srv_cancel" class="sec" hidden>Abbrechen</button> </form> </details> <details class="subsec" id="srt_lines"><summary>Leitungssteuerung</summary> <div class="sech" style="margin-top:6px">Verteilung der Netze</div> <label class="muted" style="display:block">Verteilung auf die Netze <select id="b_spread" style="width:100%;max-width:560px;margin-top:4px"><option value="best">Beste Leitung bevorzugen, andere als Reserve (empfohlen, stabiler)</option><option value="all">Alle Leitungen gleichzeitig nutzen (auch langsamere)</option></select></label> <div class="actions"><button id="b_save_spread" type="button">Verteilung speichern</button></div> </details> <details class="subsec" id="srt_rate"><summary>Bitrate und Latenz</summary> <div class="add" style="margin-top:6px"> <label class="muted">Mindestbitrate (kbit/s)<input id="b_min" type="number" min="100" max="19999" step="50" style="width:100%"></label> <label class="muted">Höchstbitrate (kbit/s)<input id="b_max" type="number" min="200" max="20000" step="100" style="width:100%"></label> <label class="muted">Latenz (ms)<input id="b_lat" type="number" min="100" max="10000" step="100" style="width:100%"></label> </div> <div class="actions"><button id="b_save" type="button">Bitrate und Latenz speichern</button></div> </details> <div id="srtlaerr" class="err"></div> <div id="srtlainfo" class="ph"></div> </details> <details class="card wide" id="c_cams" open><summary><span class="sumh">Kameras</span></summary> <details class="subsec" id="rtmp_sec"><summary>Aktive Kameras</summary> <div id="cams"></div> <div class="ph" data-hlp>Die Kamera sendet an die angezeigte RTMP-Adresse. Die Adresse gilt für die Verbindung der Kamera: je Kamera hier in der Zeile wählbar (DJI-Kameras in ihrer eigenen Karte), sonst für die Hauptverbindung, die Sie darunter bei „Hauptverbindung“ gewählt haben.</div> <div class="sech">Hauptverbindung</div> <div class="ph" style="margin-top:0">Gilt für die angezeigte RTMP-Adresse jeder Kamera ohne eigene Verbindung (Handy, Drohne, DJI-Kamera und so weiter). Eine andere Verbindung wählt man je Kamera in der Liste „Aktive Kameras“ oben; jede DJI-Kamera wählt sie unten in ihrer eigenen Karte.</div> <div class="add" style="margin-bottom:6px"><select id="netsel"></select></div> <div id="netinfo" class="ph"></div> </details> <details class="subsec" id="rtmp_new"><summary>Neue RTMP-Kamera anlegen</summary> <form id="addcam" class="add"> <input id="f_name" placeholder="Name, z. B. Hauptkamera" maxlength="40" required> <input id="f_key" placeholder="Schlüssel (leer = automatisch)" maxlength="32"> <button type="submit">Kamera hinzufügen<
10BC8
;/button> </form> <div class="qrline" id="qrnew"><select id="qrn_app" aria-label="App"></select> <button type="button" class="sec" id="qrn_go" style="padding:2px 8px;font-size:12px">QR-Code erzeugen</button> <button type="button" class="helpbtn" id="qrn_help" aria-label="Erklärung zeigen" aria-expanded="false">i</button></div> <div class="ph" id="qrn_help_txt" hidden>IRL Pro und Moblin: den Code mit der Kamera-App des Handys scannen und den Link öffnen. GoPro: zuerst den Hotspot der Box einrichten und die GoPro damit verbinden (GoPro-App), dann bei Labs-Firmware die Codes 1, 2, 3 nacheinander vor die Kamera halten. Das Handy oder die GoPro muss die Box erreichen. Der Code enthält den Kamera-Schlüssel.</div> <div id="qrn_out"></div> <div id="formerr" class="err"></div> </details> <details class="subsec" id="dji_sec"><summary>DJI-Kameras (Bluetooth)</summary><div id="djicard"> <div id="djinote" class="muted"></div> <div id="djiadapt" class="ph"></div> <div id="djicams"></div> <div class="actions"><button id="d_scan" type="button">Nach DJI-Kameras suchen</button><span id="djiscanning" class="muted" hidden></span></div> <div id="djiscan"></div> <div class="ph" data-hlp>Kamera einschalten und wach halten, Kopplung an der Kamera bestätigen. Sie tritt dem gewählten WLAN bei und streamt zu dieser Box. Unterstützt: Osmo Action 3, 4, 5 Pro, 6 und Osmo Pocket 3, 4 und 4 Pro. Welche Kamera Hauptbild oder Bild-in-Bild ist, wählen Sie in der Karte „Bildaufbau“.</div> <div id="djierr" class="err"></div> </div> </details> <details class="subsec" id="hdmi_sec"><summary>HDMI- und USB-Kameras</summary><div id="hdmicard"> <details class="dcam" id="hdmi_cam" open> <summary><span class="dname" id="hdmi_name">HDMI-Eingang</span><span class="dright"><span class="dstate" id="hdmi_state"></span></span></summary> <div class="dbody"> <label class="f">Quelle<select id="hdmi_source"><option value="hdmi">HDMI-Eingang</option><option value="usb">USB-Webcam</option></select></label> <div class="dsub muted" id="hdmi_signal"></div> <label class="chk"><input type="checkbox" id="hdmi_on"> Als Kamera senden</label> <details class="dsec" id="hdmi_vsec"><summary>Bild und Ton <span class="muted" id="hdmi_vsum"></span></summary> <div class="dgrid"> <label class="f">Bildrate<select id="hdmi_fps"><option value="30">30 fps</option><option value="25">25 fps</option></select></label> <label class="f">Bitrate (Mbit/s)<input id="hdmi_br" type="number" min="1" max="20" step="0.5"></label> <label class="f">Ton<select id="hdmi_audio"><option value="hdmi">HDMI-Ton</option><option value="none">ohne Ton</option></select></label> <label class="f" id="hdmi_fmtrow">Bildformat<select id="hdmi_fmt"><option value="auto">Automatisch (MJPEG zuerst)</option><option value="mjpeg">MJPEG (die Box kodiert mit der Bitrate neu)</option><option value="h264">H.264 (Strom der Kamera)</option></select></label> </div> <div class="actions"><button id="hdmi_save" type="button">Speichern</button></div> </details> <div id="hdmi_err" class="err"></div> </div> </details> <div class="ph" data-hlp>Das Bild wird in der Box zu einer normalen Kamera („HDMI“ oder „USB-Kamera“) und steht dann in „Bildaufbau“ wie alle anderen. USB-Webcam: eine Kamera, die sich als Webcam (UVC) meldet, zum Beispiel eine Action-Kamera im Webcam-Modus; die Box probiert die Bildformate selbst durch. Ungeprüft mit echter Hardware.</div> </div> </details> <details class="subsec" id="tw_sec"><summary>Akku-Warnung im Twitch-Chat (nur bei DJI) <span class="muted" id="tw_sum"></span></summary> <label class="chk"><input type="checkbox" id="tw_on"> Einschalten</label> <span class="muted" id="tw_acc2"></span> <div class="twbot" id="twb_row"><span class="muted" id="twb_t"></span><button type="button" id="twb_go" class="sec small">Bot-Konto anmelden</button><button type="button" id="twb_cancel" class="sec small" hidden>Abbrechen</button><button type="button" id="twb_out" class="sec small" hidden>Bot abmelden</button></div> <div id="twb_code" class="twcode" hidden><div class="twcode_txt"><div class="twcode_big" id="twb_big" translate="no"></div><a id="twb_link" href="#" target="_blank" rel="noopener">twitch.tv/activate</a></div></div> <div class="ph" data-hlp>Ein zweites Twitch-Konto (Bot) schreibt die Meldung, wenn du es anmeldest. Ohne Bot schreibt dein Hauptkonto. Beim Anmelden bei Twitch das Bot-Konto wählen, am besten in einem privaten Browserfenster. Ist der Chat nur für Follower oder Abonnenten, muss der Bot Moderator oder VIP sein.</div> <div id="tw_adv_box"> <div class="twgrid"> <label class="f sens">Kanal (Konto, auf dem gestreamt wird)<input id="tw_channel" maxlength="25" autocomplete="off" autocapitalize="off" spellcheck="false"></label> <label class="f sens" id="tw_login_l">Bot-Konto (Name)<input id="tw_login" maxlength="25" autocomplete="off" autocapitalize="off" spellcheck="false"></label> <label class="f" id="tw_token_l">Token des Bot-Kontos<input id="tw_token" type="password" maxlength="120" autocomplete="new-password"></label> </div> <div class="ph" data-hlp id="tw_manual_hint">Token mit dem Recht „chat:edit“, wie bei NOALBS. Er muss zum Bot-Konto gehören. Einfacher: im Bereich „Chat“ mit Twitch anmelden.</div> </div> <div class="twrow"> <label class="f">Warnen bei (%)<input id="tw_thr" type="number" min="1" max="50" step="1"></label> <label class="f grow">Nachricht ({Kamera} und {Prozent} werden ersetzt)<input id="tw_msg" maxlength="300" autocomplete="off"></label> </div> <label class="chk"><input type="checkbox" id="tw_live"> Nur während der Sendung</label> <div class="actions"><button id="tw_save" type="button">Speichern</button><button id="tw_test" type="button" class="sec" disabled>Testnachricht senden</button></div> <div id="tw_status" class="ph" role="status"></div> <div id="tw_err" class="err"></div> </details> <details class="subsec" id="sa_sec"><summary>Systemwarnungen im Twitch-Chat <span class="muted" id="sa_sum"></span></summary> <label class="chk"><input type="checkbox" id="sa_on"> Einschalten</label> <div class="sarows"> <label class="chk"><input type="checkbox" id="sa_t_on"> Temperatur ab <input id="sa_t" type="number" min="40" max="100" step="1" aria-label="Temperatur in Grad"> °C</label> <label class="chk"><input type="checkbox" id="sa_c_on"> CPU-Last ab <input id="sa_c" type="number" min="20" max="100" step="1" aria-label="CPU-Last in Prozent"> %</label> <label class="chk"><input type="checkbox" id="sa_r_on"> RAM ab <input id="sa_r" type="number" min="20" max="100" step="1" aria-label="Arbeitsspeicher in Prozent"> %</label> </div> <div class="sarows"> <label class="sacd">Pause <input id="sa_cd" type="number" min="1" max="120" step="1" aria-label="Pause in Minuten"> Min.</label> <label class="chk"><input type="checkbox" id="sa_rec"> „Wieder normal“ melden</label> <label class="chk"><input type="checkbox" id="sa_live"> Nur während der Sendung</label> </div> <details class="subsec" id="sa_txt"><summary>Texte</summary> <div class="twrow"> <label class="f grow">Warnung ({Name}, {Wert} und {Grenze} werden ersetzt)<input id="sa_msg_a" maxlength="300" autocomplete="off"></label> <label class="f grow">Wieder normal<input id="sa_msg_o" maxlength="300" autocomplete="off"></label> </div> </details> <div class="ph" data-hlp>Meldet über denselben Twitch-Zugang wie die Akku-Warnung (Anmeldung im Bereich „Chat“ oder Bot-Konto oben). Eine Warnung kommt, wenn der Wert etwa 30 Sekunden lang die Grenze erreicht; bleibt er darüber, folgt nach jeder Pause eine Erinnerung. „Wieder normal“ kommt, wenn er die Grenze klar unterschreitet.</div> <div class="actions"><button id="sa_save" type="button">Speichern</button><button id="sa_test" type="button" class="sec" disabled>Testnachricht senden</button></div> <div id="sa_status" class="ph" role="status"></div> <div id="sa_err" class="err"></div> </details> </details> <details class="card wide" id="pipecard" open><summary><span class="sumh">Bildaufbau: Kameras, Bild-in-Bild, Positionen</span></summary> <div class="ph" style="margin-top:0">Wie das gesendete Bild aufgebaut ist: Hauptbild und bis zu drei kleine Bilder (Bild-in-Bild) mit Position, Größe und Verzögerung. Die kleinen Bilder lassen sich unten in der Vorschau verschieben. Ohne gespeicherten Bildaufbau kann nicht gesendet werden.</div> <div class="add"> <label class="muted">Art<select id="p_type" style="width:100%"><option value="single">Eine Kamera</option><option value="pip">Bild-in-Bild</option></select></label> <label class="muted piponly">Ton von<select id="p_audio" style="width:100%"><option value="main">Hauptbild</option><option value="pip">Kleinem Bild 1</option><option value="pip2">Kleinem Bild 2</option><option value="pip3">Kleinem Bild 3</option></select></label> </div> <div class="camcols"> <div class="camcol"> <h3>Hauptbild</h3> <label class="muted">Kamera<select id="p_main" style="width:100%"></select></label> <div class="muted fixed piponly">Ecke: ganzes Bild</div> <label class="muted piponly dl" title="Das Hauptbild wartet samt Ton vor dem Einblenden. 0 = aus."><span>Verzögerung: <b id="v_delay">0</b> ms</span> <span class="dlrow"><input id="p_delay" type="range" min="0" max="3000" step="10"><input id="n_delay" type="number" min="0" max="3000" step="10"></span></label> </div> <div class="camcol piponly"> <h3>Kleines Bild 1</h3> <label class="muted">Kamera<select id="p_pip" style="width:100%"></select></label> <label class="muted">Ecke<select id="p_corner" style="width:100%"></select></label> <label class="muted" title="Skalierung dieses kleinen Bildes in Prozent des Hauptbildes (1 bis 100; 100 = so groß wie das Hauptbild). Sehr große Bilder brauchen mehr Rechenleistung.">Skalierung (%)<input id="p_size1" type="number" min="1" max="100" step="1" style="width:100%"></label> <label class="muted dl" title="Zusätzliche Wartezeit nur für das erste kleine Bild."><span>Verzögerung: <b id="v_delay1">0</b> ms</span> <span class="dlrow"><input id="p_delay1" type="range" min="0" max="3000" step="10"><input id="n_delay1" type="number" min="0" max="3000" step="10"></span></label> <details class="pvs" data-k="1"></details> </div> <div class="camcol piponly"> <h3>Kleines Bild 2 <span class="opt">(optional)</span></h3> <label class="muted">Kamera<select id="p_pip2" style="width:100%"></select></label> <label class="muted">Ecke<select id="p_corner2" style="width:100%"></select></label> <label class="muted" title="Skalierung dieses kleinen Bildes in Prozent des Hauptbildes (1 bis 100; 100 = so groß wie das Hauptbild). Sehr große Bilder brauchen mehr Rechenleistung.">Skalierung (%)<input id="p_size2" type="number" min="1" max="100" step="1" style="width:100%"></label> <label class="muted dl" id="dl2" title="Zusätzliche Wartezeit nur für das zweite kleine Bild."><span>Verzögerung: <b id="v_delay2">0</b> ms</span> <span class="dlrow"><input id="p_delay2" type="range" min="0" max="3000" step="10"><input id="n_delay2" type="number" min="0" max="3000" step="10"></span></label> <details class="pvs" data-k="2"></details> </div> <div class="camcol piponly"> <h3>Kleines Bild 3 <span class="opt">(optional)</span></h3> <label class="muted">Kamera<select id="p_pip3" style="width:100%"></select></label> <label class="muted">Ecke<select id="p_corner3" style="width:100%"></select></label> <label class="muted" title="Skalierung dieses kleinen Bildes in Prozent des Hauptbildes (1 bis 100; 100 = so groß wie das Hauptbild). Sehr große Bilder brauchen mehr Rechenleistung.">Skalierung (%)<input id="p_size3" type="number" min="1" max="100" step="1" style="width:100%"></label> <label class="muted dl" id="dl3" title="Zusätzliche Wartezeit nur für das dritte kleine Bild."><span>Verzögerung: <b id="v_delay3">0</b> ms</span> <span class="dlrow"><input id="p_delay3" type="range" min="0" max="3000" step="10"><input id="n_delay3" type="number" min="0" max="3000" step="10"></span></label> <details class="pvs" data-k="3"></details> </div> </div> <div id="dl_warn" class="ph" role="note" hidden style="color:var(--warn)">Mit „Alle Kameras immer bereit“ werden Verzögerungen über 750 ms auf 750 ms begrenzt. Größere Werte ließen das Hauptbild ruckeln.</div> <div class="piponly pvwrap"><div class="sech">Vorschau: kleine Bilder verschieben</div><div id="pvbox" class="pvbox"></div><div id="pvnote" class="ph"></div></div> <details class="subsec" id="pipe_switch"><summary>Umschalten und Tausch der Kameras</summary> <label class="muted" style="display:block;margin-top:12px"><input type="checkbox" id="p_auto" checked> Automatisch umschalten, wenn eine Kamera ausfällt (nach 5 s ohne Signal; zurück nach 60 s stabilem Signal)</label> <label class="muted piponly" style="display:block;margin-top:12px">Hauptbild tauschen ohne Unterbrechung (experimentell) <select id="p_swap" style="width:auto;margin-left:6px" title="Der Tausch läuft dann, ohne den Encoder neu zu starten: der Empfänger bekommt weiter Daten. Dafür dekodiert die Box jede Kamera der Gruppe zweimal (groß und klein)."> <option value="0">aus (Encoder startet neu, etwa 5 s ohne Bild)</option> <option value="2">Hauptbild und erstes kleines Bild</option> <option value="4">alle Kameras (mehr Rechenlast)</option></select></label> <label class="muted piponly" style="display:block;margin-top:12px"><input type="checkbox" id="p_always"> Alle Kameras immer bereit (Beta): Kameras können jederzeit dazukommen, ausfallen und zurückkehren, ohne dass die Sendung neu startet. Fällt die Hauptkamera aus, übernimmt die nächste. Ohne Kamera sendet die Box Schwarz mit Stille. Dafür dekodiert die Box jede Kamera zweimal (mehr Rechenlast).</label> </details> <details class="subsec piponly" id="pipe_facecam"><summary>Facecam</summary> <div class="ph">Wählen Sie das kleine Bild, dessen Kamera die Facecam ist. Der Ton bleibt dann bei dieser Kamera, auch wenn das Hauptbild wechselt (auch wenn die Facecam selbst zum Hauptbild getauscht wird). Mit „Keine Facecam“ folgt der Ton wie bisher immer dem Hauptbild. Die Tonquelle lässt sich jederzeit in der Fußleiste von Hand ändern; beim nächsten Tausch des Hauptbilds geht der Ton wieder zur Facecam. Die Wahl gilt sofort, ohne „Bildaufbau speichern“.</div> <div id="fc_btns" class="fcbtns"></div> <div id="fc_msg" class="ph" role="status"></div> </details> <div class="actions"><button id="p_save" type="button">Bildaufbau speichern</button><span class="muted" style="margin-left:.75rem">Läuft gerade eine Übertragung, wird sie bei einer Änderung kurz neu gestartet.</span></div> <div id="pipeerr" class="err"></div> <div id="pipeinfo" class="ph"></div> <details class="sens"><summary class="muted">Technische Beschreibung ansehen (für die Fehlersuche)</summary><pre id="pipeprev"></pre></details> </details> <details class="card wide" id="c_ctl" hidden><summary><span class="sumh">Controller-Tasten</span></summary> <div class="ph" style="margin-top:0" data-hlp>Hier legen Sie fest, was die Tasten eines gekoppelten Controllers tun. Drücken Sie eine Taste am Controller, dann wird sie in der Liste markiert. Die vier Bild-Funktionen tauschen immer das Hauptbild: Die Kamera im gewählten kleinen Bild wird zum Hauptbild, die bisherige Hauptkamera nimmt ihren Platz ein, ein zweiter Druck tauscht zurück. „Hauptbild“ holt die Hauptkamera aus der Kameraliste zurück.</div> <div id="ctl_list"></div> <div id="ctl_err" class="err"></div> </details> <details class="card wide" id="rcard"> <summary><span class="sumh">Fernzugriff (Tailscale)</span><span class="lockhint" id="rlock" hidden> · Streammodus ist an</span><span id="r_badge" class="badge">…</span></summary> <div class="ph" style="margin-top:0">Erreichen Sie die Oberfläche von unterwegs, <b>nur über Ihr privates Tailscale-Netz</b>, nie öffentlich im Internet. Jedes Gerät, das zugreifen soll, braucht die Tailscale-App und Ihr Konto.</div> <div id="r_info"></div> <div id="r_login" hidden><a id="r_loginlink" class="btnlink" target="_blank" rel="noopener noreferrer" href="#">Bei Tailscale anmelden</a> <div class="ph">Der Link öffnet Tailscale. Melden Sie sich mit Ihrem Konto an (z. B. GitHub oder Google) und klicken Sie dort auf „Connect“. Danach hier einen Moment warten.</div></div> <div id="r_url_row" class="row" hidden><span>Adresse im privaten Netz</span><span><a id="r_url" target="_blank" rel="noopener noreferrer" href="#"></a> <button id="r_copy" type="button" class="sec" style="padding:2px 8px;font-size:12px">Kopieren</button></span></div> <div id="r_https" class="pending" hidden><b>Für die Freigabe muss HTTPS im Tailscale-Konto einmal freigeschaltet sein.</b> Dort „MagicDNS“ und „HTTPS Certificates“ einschalten, danach hier „Oberfläche im privaten Netz freigeben“ drücken.<div style="margin-top:8px"><a id="r_httpslink" class="btnlink" href="https://login.tailscale.com/admin/dns" target="_blank" rel="noopener noreferrer">Tailscale-Einstellungen öffnen</a></div></div> <div id="r_warn" class="err" hidden>Achtung: „Funnel“ ist an, die Oberfläche ist öffentlich im Internet erreichbar. Mit „Freigabe beenden“ abschalten.</div> <div class="actions" id="r_actions"></div> <div id="r_msg" class="ph"></div> <details class="dsec" id="statshare"><summary>Statistik öffentlich (für OBS und Homepage)</summary> <p class="ph">Gibt <b>nur die Statistik</b> (Sendewege, Bitrate, Temperatur, Kameras …) über eine eigene öffentliche Adresse frei, zum Beispiel für eine Browserquelle in OBS oder eine Homepage. Die Oberfläche der Box bleibt davon unberührt und kann privat bleiben. Der Zugriff geht nur mit einem geheimen Schlüssel in der Adresse, höchstens 3 Betrachter gleichzeitig. Die Adresse bleibt fest, auch wenn die Box ihr Netz wechselt.</p> <div id="ss_nc" class="pending" hidden>Zuerst oben mit Tailscale verbinden.</div> <label class="chk"><input type="checkbox" id="ss_on"> Statistik öffentlich freigeben</label> <div id="ss_body" hidden> <div class="muted" style="margin-top:8px">Was darf nach außen?</div> <label class="chk"><input type="checkbox" data-ssg="send"> Senden (Bitrate, Verlust, Laufzeit)</label> <label class="chk"><input type="checkbox" data-ssg="net"> Sendewege und Netze</label> <label class="chk"><input type="checkbox" data-ssg="cameras"> Kameras</label> <label class="chk"><input type="checkbox" data-ssg="cpu"> Prozessor und Speicher</label> <label class="chk"><input type="checkbox" data-ssg="temp"> Temperaturen</label> <label class="chk"><input type="checkbox" data-ssg="alerts"> Meldungen</label> <label class="chk"><input type="checkbox" data-ssg="cores"> Prozessor-Details (Kerne, GPU, NPU)</label> <label class="chk"><input type="checkbox" data-ssg="disk"> Speicherplatz</label> <div class="muted" style="margin-top:8px">Adresse der Seite (zum Beispiel für die Browserquelle in OBS)</div> <input type="text" id="ss_page" readonly aria-label="Adresse der Statistik-Seite mit Schlüssel" style="width:100%"> <div class="muted" style="margin-top:6px">WebSocket-Adresse (für die eigene statistik.html oder andere Programme)</div> <input type="text" id="ss_ws" readonly aria-label="WebSocket-Adresse der Statistik mit Schlüssel" style="width:100%"> <div class="actions"><button type="button" id="ss_cpage" class="sec small">Adresse der Seite kopieren</button> <button type="button" id="ss_cws" class="sec small">WebSocket-Adresse kopieren</button> <button type="button" id="ss_dl" class="sec small">statistik.html herunterladen</button> <button type="button" id="ss_new" class="sec small">Neuen Schlüssel erzeugen</button></div> <div class="muted" id="ss_clients"></div> <p class="ph">Wer die Adresse kennt, sieht diese Werte. Gib sie nicht weiter und stelle die Datei nicht öffentlich ins Netz: Auf einer Homepage sehen Besucher die Adresse der Box und den Schlüssel.</p> </div> <div class="ph" id="ss_msg"></div> <div class="err" id="ss_err"></div> </details> <div class="ph" style="margin-top:14px">Tailscale-App für Ihre Geräte (offizielle Stores):</div> <div class="actions" id="r_apps"> <a class="btnlink" target="_blank" rel="noopener noreferrer" href="https://play.google.com/store/apps/details?id=com.tailscale.ipn">Android (Play Store)</a> <a class="btnlink" target="_blank" rel="noopener noreferrer" href="https://apps.apple.com/app/tailscale/id1470499037">iPhone/iPad (App Store)</a> <a class="btnlink" target="_blank" rel="noopener noreferrer" href="https://tailscale.com/download/mac">Mac</a> <a class="btnlink" target="_blank" rel="noopener noreferrer" href="https://tailscale.com/download/windows">Windows</a> <a class="btnlink" target="_blank" rel="noopener noreferrer" href="https://tailscale.com/download/linux">Linux</a> </div> <details><summary class="muted">Anleitung für Handy und Rechner</summary> <ol> <li><b>Konto:</b> Auf tailscale.com kostenlos anmelden (z. B. mit GitHub, Google, Microsoft oder Apple).</li> <li><b>Box:</b> Hier „Ja, einrichten“, dann „Verbinden“, den Link öffnen, anmelden, „Connect“ klicken.</li> <li><b>Freigabe:</b> „Oberfläche im privaten Netz freigeben“. Falls Tailscale nach „HTTPS“ fragt: den Link öffnen, „Funnel“ <b>aus</b> lassen, „Enable HTTPS“ klicken, dann erneut freigeben.</li> <li><b>Handy und Rechner:</b> Die App „Tailscale“ installieren (tailscale.com/download), mit <b>demselben Konto</b> anmelden, Verbindung einschalten.</li> <li><b>Zugriff:</b> Die Adresse oben im Browser öffnen, mit dem BELABOX-Passwort anmelden. Es ist nichts am Router freizugeben.</li> </ol></details> </details> <details class="card wide" id="swcard"> <summary><span class="sumh">Software-Update (IRL4YOU BOX)</span><span id="sw_badge" class="badge ok">…</span></summary> <div class="ph" style="margin-top:0">Holt neue Versionen dieser Software aus dem Repository auf GitHub. Kameras, Server und Einstellungen bleiben erhalten. Nicht während einer Übertragung.</div> <div class="row"><span>Installiert</span><span><b id="sw_cur">–</b><button type="button" id="sw_curinfo" class="swinfo" aria-controls="sw_hist" aria-label="Änderungen dieser Version anzeigen" title="Änderungen dieser Version anzeigen" hidden>i</button></span></div> <div class="row"><span>Neueste auf GitHub</span><b id="sw_latest">–</b></div> <div id="sw_notes" hidden></div> <div class="actions" style="margin:6px 0 0"><button id="sw_histbtn" type="button" class="sec" aria-expanded="false" aria-controls="sw_hist">Alle Änderungen</button></div> <div id="sw_hist" hidden> <input id="sw_hq" type="search" placeholder="Suchen, z. B. Vorschau, Chat, Fußleiste" aria-label="Änderungen durchsuchen" autocomplete="off" maxlength="60"> <div id="sw_hcount" class="ph"></div> <div id="sw_hres"></div> </div> <div id="sw_test" class="swpick"> <div class="sech" style="margin:0 0 6px">Datenschutz-Test (Verpixeln)</div> <div class="alert crit" id="sw_test_warn">Achtung: Das ist eine Testversion für das automatische Verpixeln von Kennzeichen und Gesichtern. Sie ist noch nicht fertig und funktioniert vorerst nur mit einer Kamera (ohne Bild-in-Bild), unter anderem wegen der Wärmeentwicklung: Sie belastet die Box deutlich stärker und heizt sie auf, das gesendete Bild kommt verzögert, und nicht alles wird erkannt. Nur zum Ausprobieren, nicht für wichtige Sendungen. Behalten Sie die Temperatur im Blick, besonders im Sommer oder in der Sonne. Zurück auf die stabile Version geht jederzeit hier.</div> <div id="sw_test_info" class="ph"></div> <label class="muted" id="sw_test_oklbl" style="display:block;margin:6px 0 0"><input type="checkbox" id="sw_test_ok"> Ich habe die Warnung gelesen.</label> <div class="actions"> <button id="sw_test_on" type="button" class="sec" disabled>Datenschutz-Test einschalten</button> <button id="sw_test_off" type="button" class="sec" hidden>Zurück auf die stabile Version</button> </div> </div> <div class="swpick"> <label class="muted" for="sw_ver">Auf eine bestimmte Version wechseln (auch ältere):</label> <div class="dlrow"><input id="sw_ver" list="sw_vers" placeholder="Version wählen oder eintragen, z. B. 0.9.0" autocomplete="off" maxlength="24"> <button id="sw_switch" type="button" class="sec">Wechseln</button></div> <datalist id="sw_vers"></datalist> <div id="sw_verinfo" class="ph"></div> </div> <div class="actions"> <button id="sw_check" type="button" class="sec">Nach Updates suchen</button> <button id="sw_install" type="button" class="sec" disabled>Update installieren</button> <button id="sw_rollback" type="button" class="sec" hidden>Zurück zur vorherigen Version</button> </div> <div id="swprog" class="prog" hidden><div class="bar"><i></i></div><div class="progline"><span><b class="progpct"></b> <span class="progtxt"></span></span><span class="progel muted"></span></div></div> <div id="sw_msg" class="ph"></div> </details> <details class="card wide" id="updcard"> <summary><span class="sumh">System-Updates (BELABOX)</span><span id="updbadge" class="badge">…</span></summary> <div id="updinfo" class="muted">lade…</div> <div class="actions"> <button id="u_check" type="button">Nach Updates suchen</button> <button id="u_dry" type="button" class="sec">Probelauf</button> <button id="u_run" type="button" class="warnbtn">Updates installieren</button> <button id="u_reboot" type="button" class="sec">Neu starten</button> </div> <div id="updprog" class="prog" hidden><div class="bar"><i></i></div><div class="progline"><span><b class="progpct"></b> <span class="progtxt"></span></span><span class="progel muted"></span></div></div> <div id="updmsg" class="ph"></div> <details id="upddet" class="sens"><summary class="muted">Protokoll</summary><pre id="updlog"></pre></details> </details> <details class="card wide" id="c_logs"><summary><span class="sumh">Protokolle</span><span id="log_badge" class="badge">…</span></summary> <div class="ph" style="margin-top:0">Eine Textdatei mit den Meldungen der Box (Kameras, Senden, Updates, Netzwerk) für die Fehlersuche, zum Beispiel für ein Issue auf GitHub. Passwörter, Stream-ID, Servername, WLAN-Namen sowie IP- und MAC-Adressen werden vorher durch Platzhalter ersetzt. Bitte vor dem Weitergeben trotzdem kurz durchsehen.</div> <div class="actions"><button id="lg_dl" type="button">Protokolle herunterladen</button> <span id="lg_msg" class="muted"></span></div> <div class="ph"><b>Speicherkarte schonen:</b> Die Box schreibt Protokolle für die Fehlersuche. Jeder Schreibvorgang belastet die Speicherkarte, die dadurch auf Dauer kaputtgehen kann.</div> <label class="logopt"><input type="radio" name="logmode" value="sparsam"> <b>Sparsam</b>: Protokolle nur im Arbeitsspeicher, kein Zugriffsprotokoll des Webservers. Schont die Speicherkarte. Nach einem Absturz oder Stromausfall bleibt <b>keine Spur</b>.</label> <label class="logopt"><input type="radio" name="logmode" value="ausfuehrlich"> <b>Ausführlich</b>: Protokolle dauerhaft auf der Karte (Journal höchstens 30 MB und 7 Tage, Zustandsprotokoll höchstens 4 MB, alle 10 Sekunden, Zugriffsprotokoll des Webservers höchstens 20 MB mit drei alten Ständen). Für die Fehlersuche, zum Beispiel wenn sich die Box von selbst ausschaltet.</label> <div id="logmsg" class="ph"></div> </details> <details class="card wide" id="c_bk"><summary><span class="sumh">Einstellungen sichern</span></summary> <div class="sech">Sichern</div> <label class="logopt"><input type="checkbox" id="bk_secrets" checked> <b>Passwörter mitnehmen</b> (WLAN, Hotspot, DJI, Stream-ID). Dann immer mit einem Passwort verschlüsselt.</label> <div class="wform bkform"> <label class="muted"><span id="bk_pwlab">Passwort zum Verschlüsseln (mindestens 8 Zeichen)</span><input type="password" id="bk_pw" autocomplete="new-password" maxlength="128"></label> <label class="muted">Passwort wiederholen<input type="password" id="bk_pw2" autocomplete="new-password" maxlength="128"></label> </div> <details class="subsec" id="bk_what"><summary>Was wird gesichert?</summary> <div class="bkwhat"> <div><div class="bkwh">Ist in der Sicherung</div><ul class="bkyes"> <li>Kameras und ihre Netzwerkkarte</li> <li>Bildaufbau und Sendeeinstellungen</li> <li>SRTLA-Server (Stream-ID nur mit dem Haken oben)</li> <li>Sendewege, Kameranetz und Hotspots (Netzwerkkarten werden über die MAC-Adresse zugeordnet)</li> <li>Gespeicherte WLAN-Netze (Passwörter nur mit dem Haken oben)</li> <li>DJI-Kameras: Name, Bildeinstellungen und Verbindung (Passwort nur mit dem Haken oben)</li> <li>HDMI-Eingang und Automatischer Start</li> <li>Eigene Namen für Verbindungen und Sticks</li> <li>Twitch: Kanal, Schwelle und Nachricht der Akku-Warnung</li> </ul></div> <div><div class="bkwh">Ist nicht dabei</div><ul class="bkno"> <li>Bluetooth-Kopplung der DJI-Kameras</li> <li>Twitch-Anmeldung und Token</li> <li>Tailscale-Verbindung und Freigaben</li> <li>Feste Zusatzadresse</li> <li>Protokolle und Mitschnitte</li> </ul></div> </div> </details> <div class="actions"><button id="bk_export" type="button">Datei herunterladen</button></div> <div id="bk_msg" class="ph" role="status"></div> <div class="sech">Einspielen</div> <div class="ph" style="margin-top:0">Datei wählen, Teile auswählen. Nur ohne Sendung. Der Stand davor wird gesichert.</div> <div class="actions"><input type="file" id="bk_file" accept=".json,application/json"></div> <div id="bk_ipwrow" class="wform bkform" hidden><label class="muted">Passwort der Datei<input type="password" id="bk_ipw" autocomplete="off" maxlength="128"></label><div class="actions"><button id="bk_open" type="button" class="sec">Öffnen</button></div></div> <div id="bk_plan" class="bkplan" hidden></div> <div id="bk_prog" class="prog" hidden><div class="bar"><i></i></div><div class="progline"><span><b class="progpct"></b> <span class="progtxt"></span></span><span class="progel muted"></span></div></div> <div id="bk_imsg" class="ph bkres" role="status"></div> <div id="bk_todo" class="bktodo" hidden> <div class="sech">Nach dem Einspielen noch zu erledigen</div> <label class="logopt"><input type="checkbox" data-todo="dji"> <b>DJI-Kameras:</b> einmal neu koppeln und verbinden (die Bluetooth-Kopplung ist nicht in der Sicherung).</label> <label class="logopt"><input type="checkbox" data-todo="twitch"> <b>Twitch:</b> in der Karte Chat neu anmelden (Anmeldung und Token sind nicht in der Sicherung).</label> <label class="logopt"><input type="checkbox" data-todo="tailscale"> <b>Tailscale:</b> in der Karte Fernzugriff neu verbinden und die Freigabe wieder einschalten.</label> <label class="logopt"><input type="checkbox" data-todo="net"> <b>Sendewege und Netzwerk für Kameras prüfen:</b> Heißen die Netzwerkkarten auf dieser Box anders (eth0, eth1), stimmen die Einstellungen nicht.</label> <div class="ph">Dein Browser merkt sich die Haken. Sind alle gesetzt, verschwindet dieser Hinweis.</div> </div> <div class="actions"><button id="bk_restore" type="button" class="sec" hidden>Letzten Stand zurückholen</button> <button id="bk_reload" type="button" class="sec" hidden>Seite neu laden</button></div> </details> <details class="card wide" id="c_dev"><summary><span class="sumh">Entwickler</span><span id="dev_badge" class="badge">…</span></summary> <div id="dev_state" class="wstat"><span class="wdot"></span><span class="wgrow" id="dev_text">Wird geladen …</span></div> <div class="actions"><button id="dev_toggle" type="button" class="warnbtn" disabled>SSH einschalten</button> <button id="dev_pw" type="button" class="sec">Passwort anzeigen</button> <button id="dev_reset" type="button" class="sec">Passwort zurücksetzen</button></div> <div id="dev_msg" class="ph" role="status"></div> <div id="dev_pw_out" class="ph sens" role="status"></div> </details> <details class="card wide" id="c_layout"><summary><span class="sumh">Optionen</span></summary> <div class="ph" style="margin-top:0">Hier legen Sie fest, welche Menüpunkte angezeigt werden (Haken weg = ausgeblendet). Ausgeblendet wird nur die Anzeige, an den Einstellungen ändert sich nichts. Die Reihenfolge der Hauptmenüs und das Design legen Sie darunter fest. Es gibt zwei Ansichten, Desktop und Mobil, mit je eigener Auswahl. Sie sind auf der Box gespeichert und gelten für alle Geräte.</div> <div id="view_ctl" class="viewctl"> <div id="view_now" class="ph" role="status"></div> <div class="row" style="justify-content:flex-start;gap:6px;flex-wrap:wrap"><label for="view_mode">Ansicht dieses Geräts:</label> <select id="view_mode" aria-label="Ansicht dieses Geräts"><option value="auto">Automatisch (nach Fensterbreite)</option><option value="desktop">Immer Desktop</option><option value="mobile">Immer Mobil</option></select></div> <div class="ph">Automatisch: bis 620 Pixel Breite (Handy, schmales Fenster) gilt Mobil, sonst Desktop. Diese Wahl gilt nur für dieses Gerät.</div> <div class="sech">Einstellungen bearbeiten für</div> <div class="vseg" role="group" aria-label="Ansicht bearbeiten"><button type="button" id="vw_desktop" aria-pressed="true">Desktop</button><button type="button" id="vw_mobile" aria-pressed="false">Mobil</button></div> <div class="actions"><button type="button" id="vw_copy" class="sec">Von Mobil übernehmen</button></div> <div id="vw_msg" class="ph" role="status"></div> </div> <details class="subsec" id="opt_design"><summary>Design</summary> <div class="ph" style="margin-top:0">Wählen Sie das Aussehen der Oberfläche für die oben gewählte Ansicht (Desktop oder Mobil). Hell oder dunkel stellen Sie wie bisher oben in der Kopfleiste ein, das gilt nur für diesen Browser.</div> <div id="design_list" class="designs" role="radiogroup" aria-label="Design"></div> </details> <details class="subsec" id="opt_menus"><summary>Menüpunkte anzeigen</summary> <div id="lay_list" class="optlist"></div> <div class="actions"><button type="button" id="lay_showall" class="sec">Alle einblenden</button></div> </details> <details class="subsec" id="opt_order"><summary>Reihenfolge der Hauptmenüs</summary> <div id="ord_list" class="optlist"></div> <div class="actions"><button type="button" id="lay_reset" class="sec">Reihenfolge zurücksetzen</button></div> </details> <details class="subsec" id="opt_netaddr"><summary>Feste Zusatzadresse (LAN)</summary> <div class="ph" style="margin-top:0">Neben der Adresse vom Router (DHCP) bekommt die Box eine feste, zum Beispiel wenn sie am selben Router hängt wie andere Geräte. Die feste Adresse gehört zur MAC-Adresse der gewählten Netzkarte. Voraussetzung ist ein getrennter Adressraum: Sie darf sich nicht mit dem Netz einer anderen Netzkarte überschneiden, kein anderes Gerät darf sie benutzen, und sie sollte außerhalb des DHCP-Bereichs deines Routers liegen. Das gilt für die ganze Box, nicht nur für dieses Gerät.</div> <label class="chk"><input type="checkbox" id="na_on"> Feste Zusatzadresse verwenden</label> <div class="row" style="justify-content:flex-start;gap:6px;flex-wrap:wrap"><select id="na_card" aria-label="Netzkarte"></select><input id="na_addr" type="text" inputmode="decimal" maxlength="15" placeholder="z. B. 192.168.1.50" aria-label="IP-Adresse" autocomplete="off" style="width:11em"><span>/</span><input id="na_prefix" type="number" min="8" max="30" value="24" aria-label="Netzlänge" style="width:4.5em"></div> <label class="chk" id="na_ack_row"><input type="checkbox" id="na_ack"> Ich bin einverstanden, dass die Änderung am Netzwerk der Box auf meine eigene Verantwortung erfolgt.</label> <div class="actions"><button type="button" id="na_save">Speichern</button></div> <div id="na_info" class="ph" role="status"></div> <div id="na_err" class="err"></div> </details> <details class="subsec" id="c_report"><summary>Problem melden / Wunsch äußern</summary> <div class="ph" style="margin-top:0">Bereitet auf GitHub ein fertig ausgefülltes Formular vor (Titel und Beschreibung sind schon eingetragen). Du meldest dich dort mit deinem eigenen GitHub-Konto an und klickst auf „Submit new issue“. Die Box sendet selbst nichts.</div> <label class="logopt"><input type="radio" name="rp_kind" value="issue" checked> <b>Problem</b> (ISSUE): Titel ist Pflicht, die Beschreibung ist freiwillig. Du bekommst eine Support-Nummer.</label> <label class="logopt"><input type="radio" name="rp_kind" value="feature"> <b>Wunsch</b> (FEATURE): Titel und Beschreibung sind Pflicht. Du bekommst ebenfalls eine Nummer.</label> <input id="rp_title" type="text" maxlength="100" placeholder="Titel" aria-label="Titel" autocomplete="off"> <textarea id="rp_desc" rows="5" maxlength="2000" placeholder="Beschreibung" aria-label="Beschreibung"></textarea> <label class="logopt" id="rp_logrow"><input type="checkbox" id="rp_log"> Bereinigtes Protokoll mitnehmen (wird als Datei geladen, die du auf GitHub anhängst; ohne Passwörter, WLAN-Namen, Adressen und Schlüssel)</label> <div class="actions"><button id="rp_go" type="button">Formular vorbereiten</button> <span id="rp_msg" class="muted"></span></div> <div id="rp_out" class="ph" hidden></div> <div class="sech">Meine Fälle</div> <div class="ph" style="margin-top:0">Deine Support-Nummern mit Datum. „Erledigt“ gilt nur hier auf der Box; das Issue auf GitHub schließt du selbst (oder der Entwickler anhand der Nummer).</div> <div id="rp_cases"></div> <div class="ph"><a id="rp_issues" href="https://github.com/IRL4YOU/irl4you-pip/issues" target="_blank" rel="noopener noreferrer">Alle Issues auf GitHub ansehen</a></div> </details> </details> <details class="card wide" id="c_power"><summary><span class="sumh">Box ausschalten und abmelden</span></summary> <div class="ph" style="margin-top:0">Fährt die Box sauber herunter oder startet sie neu. Wenn die Box ausgeschaltet ist, kann sie über die Oberfläche nicht wieder eingeschaltet werden: dafür den Strom kurz trennen oder die Ein-Taste nutzen. „Abmelden“ beendet die Anmeldung auf diesem Gerät, auch eine mit „Angemeldet bleiben“ gemerkte.</div> <div class="actions"><button id="pw_off" type="button" class="warnbtn">Herunterfahren</button> <button id="pw_reboot" type="button" class="sec">Neu starten</button> <button id="logout" type="button" class="sec" title="Von dieser Box abmelden (auch eine gemerkte Anmeldung)">Abmelden</button></div> <div id="pwmsg" class="ph"></div> </details> </main> <nav id="mfoot" aria-label="Schnellzugriff"><div id="mf_msg" class="mf-msg" role="status" hidden></div><button type="button" id="mf_live" class="mf-live" disabled aria-label="Sendung starten oder beenden">Live</button><div id="mf_tools" role="group" aria-label="Kameras und Ton" hidden></div><button type="button" id="mf_info" class="mf-info" aria-label="Bedienung der Kamera-Knöpfe" title="Bedienung der Kamera-Knöpfe" hidden>ⓘ</button></nav> <dialog id="rdlg"><form method="dialog"><b>Noch nicht übernommen</b> <p id="rdlg_t"></p><p class="muted">Die Sendung läuft noch mit den alten Werten. Ein Neustart unterbricht das Bild für einige Sekunden.</p> <div class="actions"><button type="button" id="rdlg_go" class="warnbtn">Stream neu starten</button><button type="submit" class="sec">Später</button></div></form></dialog> <div id="updtoast" class="toast" role="status" hidden></div> <footer class="foot"><div id="stagefoot"></div> <div class="footlinks">© 2026 IRL4YOU · MIT-Lizenz (Ordner srtla: AGPL-3.0) · <a href="https://discord.gg/nrBCEarMup" target="_blank" rel="noopener noreferrer">Discord</a> · <a href="https://github.com/IRL4YOU/irl4you-pip" target="_blank" rel="noopener noreferrer">Quellcode auf GitHub</a> · <a href="https://github.com/IRL4YOU/irl4you-pip/blob/main/NOTICE.md" target="_blank" rel="noopener noreferrer">Drittprojekte (Moblin, BELABOX)</a></div></footer> <script>const $=id=>document.getElementById(id);var pbUpdating=0;(function(){const f=window.fetch.bind(window);let gone=false;window.fetch=function(u,o){return f(u,o).then(r=>{if(r.status===401&&!gone&&!pbUpdating&&String(u).indexOf("/api/")===0&&String(u).indexOf("/api/login")!==0){gone=true;location.reload();}return r;});};})();const _last={};function due(key,card,idleMs){if(document.hidden)return false;const c=card&&$(card);if(!c||c.open)return true;const n=Date.now();if(!_last[key]||n-_last[key]>=idleMs){_last[key]=n;return true;}
return false;}
const fmt=(v,d=1)=>v==null?"–":Number(v).toFixed(d);const ROLE={main:"Hauptbild",pip:"Bild-in-Bild",extra:"Weitere"};const esc=t=>String(t).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));function setHtml(el,html){if(el.dataset.h===html)return;el.dataset.h=html;el.innerHTML=html;}
let camsByKey={};const camDotCls=c=>"dot"+(c.state==="live"?((c.pic==="wartet"||c.pic==="aus")?" wait":""):(c.state==="unknown"?" unk":" off"));const camDotTitle=c=>c.state!=="live"?(c.state==="unknown"?"Status unbekannt":"Kein Signal"):(c.pic==="an"?"Sendet und ist im Bild":(c.pic==="wartet"?`Sendet, kommt in ${c.pic_wait} s ins Bild`:(c.pic==="aus"?"Sendet, ist noch nicht im Bild":"Sendet")));function upDot(u){if(!u)return'<span class="dot unk" title="Nicht in Betrieb (keine Sendung)"></span>';const rtt=u.srtt==null?"":`, Laufzeit ${u.srtt} ms`;if(u.state==="an")return`<span class="dot" title="Wird genutzt${rtt}"></span>`;if(u.state==="reserve")return`<span class="dot wait" title="In Reserve${rtt} (Laufzeit zu hoch oder unruhig, es werden kaum Pakete gesendet)"></span>`;return'<span class="dot off" title="Nicht verbunden oder kein Netz"></span>';}
function battHtml(c){if(c.battery==null)return"";const col=c.battery>75?"var(--ok)":(c.battery<25?"var(--crit)":""),age=c.battery_age,stale=age!=null&&age>600;const when=stale?` (Stand vor ${age>=3600?Math.round(age/3600)+" Std.":Math.round(age/60)+" Min."})`:"";return`<span class="batt${stale?" stale":""}" style="${col?"color:"+col:""}" title="Akku ${c.battery} %${c.charging?", am Ladekabel (lädt oder wird versorgt)":""}${when}">${c.charging?"🔌 ":""}${c.battery} %</span>`;}
function paintCamDots(list){const el=$("cam_dots");if(!el)return;const l=camSorted(list||[]).filter(c=>!c.parked).slice(0,4);setHtml(el,l.map(c=>`<span class="${camDotCls(c)}" title="${esc(camDotTitle(c))}"></span>`).join(""));el.setAttribute("aria-label",l.length?l.map(c=>c.name).join(", "):"");}
const camSeenLive={};const CAM_HOLD_TOP_MS=120000;function camSorted(list){const nowMs=Date.now();for(const c of list)if(c.state==="live"||c.state==="unknown")camSeenLive[c.id]=nowMs;const quiet=c=>c.parked?2:(c.state==="live"||c.state==="unknown"||nowMs-(camSeenLive[c.id]||0)<CAM_HOLD_TOP_MS?0:1);return list.slice().sort((x,y)=>quiet(x)-quiet(y));}
function renderCamLights(list){list=camSorted(list).filter(c=>!c.parked);const any=list.some(c=>c.battery!=null),box=$("camlights");box.classList.toggle("hasbatt",any);const head='<div class="hrow"><div class="sech">Kameras</div>'+(any&&list.length?'<div class="sech mid">Akku</div><div class="sech"> </div>':"")+'</div>';setHtml(box,list.length?head+list.map(c=>`<div class="row"><span class="nm"><span class="${camDotCls(c)}" title="${esc(camDotTitle(c))}"></span>${esc(c.name)}</span>${any?`<span class="battcell">${battHtml(c)}</span>`:""}<span>${c.state==="live"?`<b>${fmt(c.mbit)}</b><span class="unit">Mbit/s</span>`:'<span class="unit">–</span>'}</span></div>`).join(""):'<div class="muted">Noch keine Kamera angelegt.</div>');}
let netOpts=[];const netLabel=o=>`${o.label}${connNames[o.iface]?" ("+connNames[o.iface]+")":""} · ${o.cam_ip||o.ip}${o.cam_ip&&o.cam_ip!==o.ip?" (feste Adresse)":""}`;function camNetHtml(c){if(c.via_src==="hdmi")return`<span class="camnet muted" title="Das Bild kommt vom HDMI-Eingang der Box">HDMI-Eingang</span>`;if((c.key||"").startsWith("dji-")){const own=c.via_src==="dji";const nets=netOpts.filter(o=>o.wifi!==false||(own&&c.via===o.iface));const opts=[`<option value=""${own?"":" selected"}>– wählen –</option>`].concat(nets.map(o=>`<option value="${esc(o.iface)}"${own&&c.via===o.iface?" selected":""}>${esc(netLabel(o))}</option>`));if(own&&c.via==="manual")opts.push(`<option value="manual" selected disabled>Manuell (in der DJI-Karte)</option>`);else if(own&&c.via&&!netOpts.some(o=>o.iface===c.via))opts.push(`<option value="${esc(c.via)}" selected>${esc(c.via)} (nicht verbunden)</option>`);return`<span class="camnet withbtn"><select class="camnetsel" data-dji="${esc(c.key)}" title="Verbindung dieser Kamera: Ihre Adresse gilt für dieses Netz. Eine Änderung gilt ab der nächsten Verbindung der Kamera." aria-label="Verbindung">${opts.join("")}</select><button type="button" class="camreconn sec" data-dji="${esc(c.key)}" title="Neu verbinden: Die geänderte Verbindung gilt erst danach (der Stream der Kamera ist kurz weg)" aria-label="Neu verbinden">↻</button></span>`;}
const opts=[`<option value="">Hauptverbindung (Standard)</option>`].concat(netOpts.map(o=>`<option value="${esc(o.iface)}"${c.iface===o.iface?" selected":""}>${esc(netLabel(o))}</option>`));if(c.iface&&!netOpts.some(o=>o.iface===c.iface))opts.push(`<option value="${esc(c.iface)}" selected>${esc(c.iface)} (nicht verbunden)</option>`);return`<span class="camnet"><select class="camnetsel" data-id="${c.id}" title="Verbindung dieser Kamera: Ihre Adresse gilt für dieses Netz" aria-label="Verbindung">${opts.join("")}</select></span>`;}
const QR=(()=>{const EXP=new Uint8Array(512),LOG=new Uint8Array(256);for(let i=0,x=1;i<255;i++){EXP[i]=x;LOG[x]=i;x<<=1;if(x&256)x^=0x11d;}
for(let i=255;i<512;i++)EXP[i]=EXP[i-255];const mul=(a,b)=>a&&b?EXP[LOG[a]+LOG[b]]:0;const gen=n=>{let g=[1];for(let i=0;i<n;i++){const r=new Array(g.length+1).fill(0);for(let j=0;j<g.length;j++){r[j]^=g[j];r[j+1]^=mul(g[j],EXP[i]);}g=r;}return g;};const rs=(data,n)=>{const g=gen(n),r=new Array(n).fill(0);for(const d of data){const f=d^r.shift();r.push(0);if(f)for(let i=0;i<n;i++)r[i]^=mul(g[i+1],f);}return r;};const V=[null,[26,10,[[1,16]]],[44,16,[[1,28]]],[70,26,[[1,44]]],[100,18,[[2,32]]],[134,24,[[2,43]]],[172,16,[[4,27]]],[196,18,[[4,31]]],[242,22,[[2,38],[2,39]]],[292,22,[[3,36],[2,37]]],[346,26,[[4,43],[1,44]]]];const VL=[null,[26,7,[[1,19]]],[44,10,[[1,34]]],[70,15,[[1,55]]],[100,20,[[1,80]]],[134,26,[[1,108]]],[172,18,[[2,68]]],[196,20,[[2,78]]],[242,24,[[2,97]]],[292,30,[[2,116]]],[346,18,[[2,68],[2,69]]]];const AL=[null,[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50]];const MASK=[(x,y)=>(x+y)%2===0,(x,y)=>y%2===0,(x,y)=>x%3===0,(x,y)=>(x+y)%3===0,(x,y)=>(Math.floor(x/3)+(y>>1))%2===0,(x,y)=>x*y%2+x*y%3===0,(x,y)=>(x*y%2+x*y%3)%2===0,(x,y)=>((x+y)%2+x*y%3)%2===0];function penalty(m,n){let p=0,dark=0;const line=get=>{let run=1;for(let i=1;i<=n;i++){if(i<n&&get(i)===get(i-1))run++;else{if(run>=5)p+=run-2;run=1;}}
for(let i=0;i+11<=n;i++){const w=[];for(let k=0;k<11;k++)w.push(get(i+k));const s=w.join("");if(s==="10111010000"||s==="00001011101")p+=40;}};for(let y=0;y<n;y++){line(i=>m[y][i]);line(i=>m[i][
10BC8
y]);for(let x=0;x<n;x++){dark+=m[y][x];if(x&&y&&m[y][x]===m[y][x-1]&&m[y][x]===m[y-1][x]&&m[y][x]===m[y-1][x-1])p+=3;}}
return p+Math.floor(Math.abs(dark*20-n*n*10)/(n*n))*10;}
function make(text,V,ecb){const bytes=[...new TextEncoder().encode(text)];let v=1,cap=0;for(;v<=10;v++){cap=V[v][2].reduce((a,[c,d])=>a+c*d,0);if(bytes.length<=(cap*8-(v<10?12:20))>>3)break;}
if(v>10)return null;const bits=[],put=(val,k)=>{for(let i=k-1;i>=0;i--)bits.push((val>>i)&1);};put(4,4);put(bytes.length,v<10?8:16);bytes.forEach(b=>put(b,8));put(0,Math.min(4,cap*8-bits.length));while(bits.length%8)bits.push(0);for(let q=0;bits.length<cap*8;q^=1)put(q?0x11:0xec,8);const cw=[];for(let i=0;i<bits.length;i+=8)cw.push(parseInt(bits.slice(i,i+8).join(""),2));const[,ecn,groups]=V[v],blocks=[];let pos=0;for(const[c,d]of groups)for(let i=0;i<c;i++){const data=cw.slice(pos,pos+d);pos+=d;blocks.push({data,ec:rs(data,ecn)});}
const all=[];for(let i=0;i<Math.max(...blocks.map(b=>b.data.length));i++)for(const b of blocks)if(i<b.data.length)all.push(b.data[i]);for(let i=0;i<ecn;i++)for(const b of blocks)all.push(b.ec[i]);const n=17+4*v,B=[...Array(n)].map(()=>new Array(n).fill(0)),fn=[...Array(n)].map(()=>new Array(n).fill(false));const set=(x,y,val)=>{B[y][x]=val?1:0;fn[y][x]=true;};const finder=(cx,cy)=>{for(let dy=-4;dy<=4;dy++)for(let dx=-4;dx<=4;dx++){const x=cx+dx,y=cy+dy;if(x<0||y<0||x>=n||y>=n)continue;const d=Math.max(Math.abs(dx),Math.abs(dy));set(x,y,d<=1||d===3);}};finder(3,3);finder(n-4,3);finder(3,n-4);for(let i=8;i<n-8;i++){set(i,6,i%2===0);set(6,i,i%2===0);}
const al=AL[v];for(const cy of al)for(const cx of al){if((cx===6&&cy===6)||(cx===6&&cy===al[al.length-1])||(cy===6&&cx===al[al.length-1]))continue;for(let dy=-2;dy<=2;dy++)for(let dx=-2;dx<=2;dx++)set(cx+dx,cy+dy,Math.max(Math.abs(dx),Math.abs(dy))!==1);}
for(let i=0;i<9;i++){if(i!==6){fn[8][i]=true;fn[i][8]=true;}}
for(let i=0;i<8;i++){fn[8][n-1-i]=true;fn[n-1-i][8]=true;}
set(8,n-8,1);if(v>=7)for(let i=0;i<18;i++){const a=n-11+i%3,b=Math.floor(i/3);fn[b][a]=true;fn[a][b]=true;}
const bo=[];all.forEach(b=>{for(let i=7;i>=0;i--)bo.push((b>>i)&1);});let k=0;for(let right=n-1;right>=1;right-=2){if(right===6)right=5;for(let vert=0;vert<n;vert++)for(let j=0;j<2;j++){const x=right-j,y=((right+1)&2)===0?n-1-vert:vert;if(!fn[y][x]){B[y][x]=k<bo.length?bo[k]:0;k++;}}}
const vbits=(()=>{if(v<7)return 0;let r=v;for(let i=0;i<12;i++)r=(r<<1)^((r>>11)*0x1f25);return(v<<12)|r;})();let best=null,bestP=1e9;for(let mk=0;mk<8;mk++){const m=B.map(r=>r.slice());for(let y=0;y<n;y++)for(let x=0;x<n;x++)if(!fn[y][x]&&MASK[mk](x,y))m[y][x]^=1;const fd=(ecb<<3)|mk;let r=fd;for(let i=0;i<10;i++)r=(r<<1)^((r>>9)*0x537);const fb=((fd<<10)|r)^0x5412,bit=i=>(fb>>i)&1;for(let i=0;i<=5;i++)m[i][8]=bit(i);m[7][8]=bit(6);m[8][8]=bit(7);m[8][7]=bit(8);for(let i=9;i<15;i++)m[8][14-i]=bit(i);for(let i=0;i<8;i++)m[8][n-1-i]=bit(i);for(let i=8;i<15;i++)m[n-15+i][8]=bit(i);m[n-8][8]=1;if(v>=7)for(let i=0;i<18;i++){const a=n-11+i%3,b=Math.floor(i/3),t=(vbits>>i)&1;m[b][a]=t;m[a][b]=t;}
const pn=penalty(m,n);if(pn<bestP){bestP=pn;best=m;}}
return best;}
const matrix=text=>make(text,V,0)||make(text,VL,1);const svg=text=>{const m=matrix(text);if(!m)return"";const n=m.length;let d="";for(let y=0;y<n;y++)for(let x=0;x<n;x++)if(m[y][x])d+=`M${x+4} ${y+4}h1v1h-1z`;return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n+8} ${n+8}" width="220" height="220" shape-rendering="crispEdges" role="img" aria-label="QR-Code"><rect width="100%" height="100%" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;};return{matrix,svg};})();const larixLink=(url,name)=>"larix://set/v1?conn[][url]="+encodeURIComponent(url)+"&conn[][name]="+encodeURIComponent(name);const QR_APPS=[["irlpro","IRL Pro"],["moblin","Moblin"],["gopro","GoPro (HERO 8–11)"],["gopro13","GoPro (HERO 12/13)"]];const qrApp={},qrOut={},qrHelp=new Set();const qrBox=(items,big)=>`<div class="qrbox${big?" big":""}">${items.map(([svg,label])=>`<div class="qri">${svg||'<span class="muted">zu lang</span>'}${label?`<span class="muted">${label}</span>`:""}</div>`).join("")}</div>`;async function qrBuild(c,app){if(app==="irlpro")return qrBox([[QR.svg(larixLink(c.url,c.name)),""]]);if(app==="moblin")return qrBox([[QR.svg("moblin://?"+encodeURIComponent(JSON.stringify({streams:[{name:c.name,url:c.url,video:{codec:"H.264/AVC"}}]}))),""]]);const items=[];const hs=((wifiData&&wifiData.cards)||[]).find(x=>x.hotspot&&x.hotspot.running);let url=c.url,note="";if(hs){try{const r=await srtlaCall("GET","/api/wifi/hotspot?iface="+encodeURIComponent(hs.iface));items.push([QR.svg('!MJOIN="'+r.ssid+":"+r.password+'"'),"WLAN"]);if(hs.ip)url=c.url.replace(/^(rtmp:\/\/)[^:\/]+/,"$1"+hs.ip);}catch(x){}}
else note='<div class="muted">Ohne Hotspot gibt es keinen WLAN-Code: Unter WLAN am Stick „Hotspot“ einschalten.</div>';items.push([QR.svg('!MRTMP="'+url+'"'),"RTMP"]);items.push([QR.svg((app==="gopro13"?"":"oW1mVr1080!W")+"!GL"),"Start"]);items.forEach((it,i)=>{it[1]=(i+1)+" "+it[1];});return qrBox(items,true)+note;}
let lastCams=[];const camOffBtn=c=>{if(!(c.in_pips||c.is_main||(c.key||"").startsWith("dji-")))return"";const t=c.is_main?"Das Hauptbild lässt sich nicht ausschalten. Zuerst eine andere Kamera zum Hauptbild machen.":(c.parked?"Nimmt die Kamera wieder ins Bild und verbindet sie (DJI).":"Nimmt die Kamera aus dem Bild und trennt sie (DJI), ohne dass eine Störungsmeldung kommt. Sie bleibt gespeichert und lässt sich wieder einschalten.");return`<button type="button" class="sec camoff" data-id="${c.id}" data-on="${c.parked?1:0}" title="${esc(t)}"${c.is_main?" disabled":""}>${c.parked?"Einschalten":"Ausschalten"}</button>`;};$("cams").addEventListener("click",async e=>{const b=e.target.closest(".camoff");if(!b||b.disabled)return;const c=lastCams.find(x=>x.id===b.dataset.id);if(!c)return;const wasOff=b.dataset.on==="1";b.disabled=true;try{if(c.in_pips)await srtlaCall("POST","/api/pipeline/active",{key:c.key,active:wasOff});if((c.key||"").startsWith("dji-")){const dc=(djiData.cameras||[]).find(x=>x.rtmp_key===c.key);if(dc)await djiCmd({cmd:wasOff?"connect":"disconnect",addr:dc.addr});}}catch(x){alert(x.message);}
tick();if(typeof djiLoad==="function")djiLoad();});const qrCan=c=>!!c.url&&c.via_src!=="hdmi"&&!(c.key||"").startsWith("dji-");var camDragging=false;function renderCams(list){if(camDragging)return;const box=$("cams");lastCams=list;camsByKey={};for(const c of list)camsByKey[c.key]=c;list=camSorted(list);const sig=JSON.stringify([list.map(c=>[c.id,c.name,c.role,c.url,!!c.auto,c.via,c.via_src,c.iface||"",!!c.parked,!!c.is_main,!!c.in_pips]),netOpts,qrOut,[...qrHelp],connNames]);const mv=(c,i,dir)=>{const o=list[i+dir],t=dir<0?"Kamera nach oben schieben":"Kamera nach unten schieben";return`<button type="button" class="mv" data-id="${c.id}" data-with="${o?o.id:""}" title="${t}" aria-label="${t}"${o?"":" disabled"}>${dir<0?"▲":"▼"}</button>`;};if(box.dataset.sig!==sig){box.dataset.sig=sig;box.innerHTML=list.length?list.map((c,i)=>`<div class="cam" data-cid="${c.id}"><div class="top"><span class="cl"><span class="grip" role="button" tabindex="0" title="Zum Verschieben ziehen" aria-label="Kamera verschieben (ziehen oder mit den Pfeiltasten)">⋮⋮</span><span class="dot"></span><a href="#" class="ren" data-id="${c.id}" data-name="${esc(c.name)}">${esc(c.name)}</a><button type="button" class="sec ren" data-id="${c.id}" data-name="${esc(c.name)}" title="Kamera umbenennen" style="padding:2px 8px;font-size:12px;margin-left:6px">Umbenennen</button>${c.auto?'<span class="tag">automatisch erkannt</span>':""}${c.parked?'<span class="tag">aus (gewollt)</span>':""}</span>
<span class="cn">${camNetHtml(c)}</span>
<span class="muted cr"><span class="dyn"></span> ${mv(c,i,-1)}${mv(c,i,1)}${camOffBtn(c)}<button class="del" data-id="${c.id}">Entfernen</button></span></div>${(c.key||"").startsWith("dji-")?`<div class="netnote"data-key="${esc(c.key)}"role="status"hidden></div><div class="netlock"data-lockkey="${esc(c.key)}"hidden></div>`:""}${c.url?`<code>${esc(c.url)}</code>`:""}${qrCan(c)?`<div class="qrline"><select class="qrsel"data-id="${c.id}"aria-label="App">${QR_APPS.map(a=>`<option value="${a[0]}"${(qrApp[c.id]||"irlpro")===a[0]?" selected":""}>${a[1]}</option>`).join("")}</select><button type="button"class="sec qrb"data-id="${c.id}"style="padding:2px 8px;font-size:12px">QR-Code erzeugen</button><button type="button"class="helpbtn qrhb"data-id="${c.id}"aria-label="Erklärung zeigen"aria-expanded="${qrHelp.has(c.id)}">?</button></div>${qrHelp.has(c.id)?`<div class="ph">IRL Pro und Moblin: den Code mit der Kamera-App des Handys scannen und den Link öffnen. GoPro: zuerst den Hotspot der Box einrichten und die GoPro damit verbinden (GoPro-App), dann bei Labs-Firmware die Codes 1, 2, 3 nacheinander vor die Kamera halten. Das Handy oder die GoPro muss die Box erreichen. Der Code enthält den Kamera-Schlüssel.</div>`:""}${qrOut[c.id]||""}`:""}</div>`).join(""):'<div class="muted">Noch keine Kamera angelegt.</div>';}
for(const c of list){const row=box.querySelector(`.cam[data-cid="${c.id}"]`);if(!row)continue;row.querySelector(".dot").className=camDotCls(c);const dot=row.querySelector(".dot");row.querySelector(".dyn").textContent=c.state==="live"?`${c.fps?fmt(c.fps,0)+" fps"+" · ":""}${fmt(c.mbit)} Mbit/s`:(c.state==="unknown"?"Status unbekannt":"wartet auf Signal");dot.title=camDotTitle(c);}
paintNetNotes();}
const netChg={};const NET_BUSY=["searching","connecting","pairing","preparing","wifi","configuring","starting"];function netNoteOf(key){const d=((typeof djiData!=="undefined"&&djiData.cameras)||[]).find(x=>x.rtmp_key===key);if(!d)return null;const ch=netChg[key];if(!ch)return null;const st=d.state;if(NET_BUSY.includes(st))ch.busy=true;if(st==="error"){const det=String(d.detail||"");if(/von Hand|Passwort/i.test(det))return{cls:"bad",t:"Bitte WLAN-Name und Passwort in der DJI-Karte eintragen."};return{cls:"bad",t:"Netzwechsel fehlgeschlagen. Die Meldung steht in der DJI-Karte."};}
if(NET_BUSY.includes(st)||(ch.reconn&&!ch.busy&&st==="streaming"))return{cls:"",t:"Wechsle Netz …"};if(st==="streaming"){if(ch.busy&&d.wifi_ifname===ch.iface){if(!ch.okAt)ch.okAt=Date.now();if(Date.now()-ch.okAt>15000){delete netChg[key];return null;}
return{cls:"ok",t:`Netzwechsel erfolgreich (${ch.iface})`};}
return{cls:"",t:"Netz gewählt. Gilt nach „Neu verbinden“ (↻)."};}
return{cls:"",t:"Netz gewählt. Gilt beim nächsten Verbinden."};}
function paintNetNotes(){document.querySelectorAll("#cams .netnote").forEach(el=>{const key=el.dataset.key,n=netNoteOf(key);const d=((typeof djiData!=="undefined"&&djiData.cameras)||[]).find(x=>x.rtmp_key===key),lock=!!(d&&d.switch_wait>0);document.querySelectorAll(`#cams [data-dji="${key}"]`).forEach(b=>{if(!(b.classList.contains("camnetsel")||b.classList.contains("camreconn")))return;if(b.disabled!==lock)b.disabled=lock;if(lock){if(b.dataset.ot===undefined)b.dataset.ot=b.title;const t=`Wechsel für zwei Minuten gesperrt: noch ${d.switch_wait} s.`;if(b.title!==t)b.title=t;}
else if(b.dataset.ot!==undefined){b.title=b.dataset.ot;delete b.dataset.ot;}});const lk=document.querySelector(`#cams .netlock[data-lockkey="${key}"]`);if(lk){const show=lock&&!n,t=show?`Wechsel für zwei Minuten gesperrt: noch ${d.switch_wait} s.`:"";lk.hidden=!show;if(lk.textContent!==t)lk.textContent=t;}
el.hidden=!n;if(!n)return;if(el.textContent!==n.t)el.textContent=n.t;el.className="netnote"+(n.cls?" "+n.cls:"");});}
setInterval(()=>{if(!document.hidden)paintNetNotes();},1000);$("cams").addEventListener("click",async e=>{if(!e.target.classList.contains("del"))return;const id=e.target.dataset.id;if(!confirm("Kamera entfernen? Eine DJI-Kamera wird dabei auch aus der DJI-Karte entfernt."))return;await fetch("/api/cameras/"+id,{method:"DELETE"});tick();});$("qrn_app").innerHTML=QR_APPS.map(a=>`<option value="${a[0]}">${a[1]}</option>`).join("");const qrnId={};$("qrn_app").addEventListener("change",()=>{$("qrn_out").innerHTML="";});$("qrn_help").addEventListener("click",()=>{const h=$("qrn_help_txt");h.hidden=!h.hidden;$("qrn_help").setAttribute("aria-expanded",String(!h.hidden));});$("qrn_go").addEventListener("click",async()=>{const out=$("qrn_out");if(out.innerHTML){out.innerHTML="";return;}
const app=$("qrn_app").value,base=((QR_APPS.find(a=>a[0]===app)||[0,"Kamera"])[1]).replace(/ \(.*\)/,"");const metrics=async()=>(await(await fetch("/api/metrics")).json()).cameras||[];let list=await metrics(),c=list.find(x=>x.id===qrnId[app]);if(!c){const n=list.filter(x=>(x.name||"").startsWith(base)).length+1;const r=await fetch("/api/cameras",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:base+" "+n,key:""})});if(!r.ok){$("formerr").textContent=(await r.json().catch(()=>({}))).error||"";return;}
const cam=await r.json();qrnId[app]=cam.id;list=await metrics();c=list.find(x=>x.id===cam.id);}
if(!c||!c.url)return;out.innerHTML=await qrBuild(c,app);tick();});$("cams").addEventListener("click",e=>{const h=e.target.closest(".qrhb");if(!h)return;if(qrHelp.has(h.dataset.id))qrHelp.delete(h.dataset.id);else qrHelp.add(h.dataset.id);renderCams(lastCams);});$("cams").addEventListener("click",async e=>{const b=e.target.closest(".qrb");if(!b)return;const id=b.dataset.id,c=lastCams.find(x=>x.id===id);if(!c)return;if(qrOut[id]){delete qrOut[id];renderCams(lastCams);return;}
const sel=b.parentNode.querySelector(".qrsel");qrApp[id]=sel?sel.value:"irlpro";qrOut[id]=await qrBuild(c,qrApp[id]);renderCams(lastCams);});$("cams").addEventListener("change",e=>{const t=e.target.closest(".qrsel");if(!t)return;qrApp[t.dataset.id]=t.value;delete qrOut[t.dataset.id];renderCams(lastCams);});(function camDragInit(){const box=$("cams");let drag=null;const DIRKEY=Object.assign(Object.create(null),{ArrowUp:-1,ArrowDown:1});const rows=()=>[...box.children].filter(r=>r.classList.contains("cam"));let bar=null;const clear=()=>{if(bar){bar.remove();bar=null;}};async function save(row,beforeId){const r=await fetch("/api/cameras/"+row.dataset.cid,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({move_before:beforeId||null})});if(!r.ok)alert((await r.json().catch(()=>({}))).error||"Verschieben fehlgeschlagen");box.dataset.sig="";camDragging=false;await tick();}
function target(){const row=drag.row,sy=window.scrollY||0,center=drag.centerPage+(drag.y+sy-drag.startPage);for(const o of rows()){if(o===row)continue;const b=o.getBoundingClientRect();if(b.top+sy+b.height/2>center)return o;}
return null;}
function show(){const row=drag.row,t=target(),all=rows(),last=all[all.length-1];if(!bar){bar=document.createElement("div");bar.className="camdrop";box.appendChild(bar);}
bar.style.top=(t?t.offsetTop:last.offsetTop+last.offsetHeight)+"px";const dy=drag.y+(window.scrollY||0)-drag.startPage;row.style.transform="translateY("+dy+"px)";}
box.addEventListener("pointerdown",e=>{const g=e.target.closest&&e.target.closest(".grip");if(!g||(e.pointerType==="mouse"&&e.button!==0))return;const row=g.closest(".cam");if(!row||rows().length<2)return;e.preventDefault();camDragging=true;const b=row.getBoundingClientRect(),sy=window.scrollY||0;drag={row,id:e.pointerId,moved:false,y:e.clientY,startPage:e.clientY+sy,centerPage:b.top+sy+b.height/2};try{g.setPointerCapture(e.pointerId);}catch(x){}
row.classList.add("dragging");document.body.classList.add("camdrag");});box.addEventListener("pointermove",e=>{if(!drag||e.pointerId!==drag.id)return;drag.y=e.clientY;if(Math.abs(e.clientY+(window.scrollY||0)-drag.startPage)>3)drag.moved=true;if(drag.moved)show();if(e.clientY<60)window.scrollBy(0,-12);else if(e.clientY>innerHeight-60)window.scrollBy(0,12);});const end=e=>{if(!drag||e.pointerId!==drag.id)return;const row=drag.row,moved=drag.moved,t=moved&&e.type!=="pointercancel"?target():undefined;drag=null;row.classList.remove("dragging");row.style.transform="";clear();document.body.classList.remove("camdrag");const all=rows(),next=all[all.indexOf(row)+1]||null;if(t===undefined||t===next){camDragging=false;if(moved){box.dataset.sig="";tick();}return;}
save(row,t?t.dataset.cid:null);};box.addEventListener("pointerup",end);box.addEventListener("pointercancel",end);box.addEventListener("keydown",e=>{const g=e.target.closest&&e.target.closest(".grip"),dir=DIRKEY[e.key];if(!g||!dir)return;e.preventDefault();const row=g.closest(".cam"),all=rows(),i=all.indexOf(row),j=i+dir;if(j<0||j>=all.length)return;camDragging=true;const before=dir<0?all[j]:(all[j+1]||null);save(row,before?before.dataset.cid:null).then(()=>{const n=box.querySelector(`.cam[data-cid="${row.dataset.cid}"] .grip`);if(n)n.focus();});});})();$("cams").addEventListener("click",async e=>{const b=e.target.closest(".mv");if(!b||b.disabled||!b.dataset.with)return;const r=await fetch("/api/cameras/"+b.dataset.id,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({swap_with:b.dataset.with})});if(!r.ok)alert((await r.json().catch(()=>({}))).error||"Verschieben fehlgeschlagen");tick();});const djiAddrOfKey=key=>{const c=((typeof djiData!=="undefined"&&djiData.cameras)||[]).find(x=>x.rtmp_key===key);return c?c.addr:null;};$("cams").addEventListener("change",async e=>{if(!e.target.classList.contains("camnetsel"))return;if(e.target.dataset.dji){const addr=djiAddrOfKey(e.target.dataset.dji);if(!addr||!e.target.value){if(!addr)alert("Die Kamera ist im Kamera-Dienst nicht bekannt.");return;}
netChg[e.target.dataset.dji]={iface:e.target.value,reconn:false,busy:false,okAt:0};paintNetNotes();djiUpdate(addr,{wifi_ifname:e.target.value});setTimeout(()=>{tick();djiLoad();},900);return;}
const r=await fetch("/api/cameras/"+e.target.dataset.id,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({iface:e.target.value})});if(!r.ok)alert((await r.json().catch(()=>({}))).error||"Verbindung konnte nicht gesetzt werden");tick();});$("cams").addEventListener("click",async e=>{if(e.target.classList.contains("camreconn")){const addr=djiAddrOfKey(e.target.dataset.dji);if(!addr){alert("Die Kamera ist im Kamera-Dienst nicht bekannt.");return;}
const key=e.target.dataset.dji,dc=((typeof djiData!=="undefined"&&djiData.cameras)||[]).find(x=>x.rtmp_key===key);netChg[key]=Object.assign(netChg[key]||{iface:dc?dc.wifi_ifname:"",busy:false,okAt:0},{reconn:true,busy:false,okAt:0});if(dc&&!netChg[key].iface)netChg[key].iface=dc.wifi_ifname;e.target.blur();await djiCmd({cmd:"reconnect",addr});paintNetNotes();setTimeout(()=>{tick();djiLoad();},900);return;}
if(!e.target.classList.contains("ren"))return;e.preventDefault();await renameCam(e.target.dataset.id,e.target.dataset.name);});async function renameCam(id,old){const n=prompt("Neuer Name der Kamera (z. B. Rucksack, Brust, Helm):",old);if(!n||n.trim()===old)return;const r=await fetch("/api/cameras/"+id,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n})});if(!r.ok)alert((await r.json().catch(()=>({}))).error||"Umbenennen fehlgeschlagen");tick();}
$("addcam").addEventListener("submit",async e=>{e.preventDefault();$("formerr").textContent="";const r=await fetch("/api/cameras",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:$("f_name").value,key:$("f_key").value})});if(!r.ok){$("formerr").textContent=(await r.json()).error;return;}
$("f_name").value="";$("f_key").value="";tick();});$("logout").addEventListener("click",async()=>{await fetch("/api/logout",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});location.reload();});const UPD_LABEL={check:"Suche",autocheck:"Suche",dry:"Probelauf",run:"Update",reboot:"Neustart"};const LOC=()=>window.PB_I18N?PB_I18N.locale():"de-DE";const fmtT=t=>t?new Date(t*1000).toLocaleString(LOC()):"nie";function progSet(id,show,pct,text,since){const b=$(id);if(!b)return;b.hidden=!show;if(!show){b.dataset.since="";return;}
if(!b.dataset.since||since)b.dataset.since=String(since||b.dataset.since||Date.now());b.querySelector(".bar>i").style.width=Math.max(4,Math.min(100,pct||0))+"%";b.querySelector(".progpct").textContent=pct>0?pct+" %":"";b.querySelector(".progtxt").textContent=text||"";progTick();}
function progTick(){document.querySelectorAll(".prog:not([hidden])").forEach(b=>{const s=+b.dataset.since;if(!s)return;const sec=Math.max(0,Math.floor((Date.now()-s)/1000)),el=b.querySelector(".progel");el.textContent="⏱ "+Math.floor(sec/60)+":"+String(sec%60).padStart(2,"0");});}
setInterval(progTick,1000);function renderUpdate(u){const v=u.versions||{},busy=u.state==="running"||u.state==="rebooting";const lines=[["Kernel (läuft)",esc(u.kernel_running||"–")],["Kernelpaket installiert",esc(v["belabox-linux-rk3588"]||"–")],["belaui / belacoder",esc((v["belaui"]||"–")+" / "+(v["belacoder"]||"–"))],["Zuletzt geprüft",fmtT(u.last_check)]];if(u.available!=null)lines.push(["Verfügbar",u.download&&u.download!=="0 B"?`${u.available} Pakete (${esc(u.download)})`:`${u.available} Pakete`]);if(u.available!=null&&u.belabox&&u.belabox.length)lines.push(["BELABOX-Pakete",u.belabox.map(esc).join(", ")]);if(u.would_reboot)lines.push(["Hinweis","Danach ist ein Neustart nötig (Kernel/Netzwerk)."]);$("updinfo").innerHTML=lines.map(l=>`<div class="upd-line"><span class="muted">${l[0]}</span><span>${l[1]}</span></div>`).join("");{const dot=$("hdr_sys"),n=(!busy&&u.state!=="failed"&&u.state!=="refused"&&u.available>0)?u.available:0;dot.hidden=!n;if(n){const t=[u.last_check?`BELABOX-Systemupdates: ${n} Pakete verfügbar (Stand ${fmtT(u.last_check)}).`:`BELABOX-Systemupdates: ${n} Pakete verfügbar.`,u.would_reboot?"Danach ist ein Neustart nötig.":"",u.streaming?"Einspielen geht nicht während der Übertragung.":"Zu den Systemupdates."].filter(Boolean).join(" ");dot.title=t;dot.setAttribute("aria-label",t);}}
const p=u.progress,show=u.state==="running"&&p&&p.total>0;if(show)progSet("updprog",true,Math.round(100*(p.downloading+p.unpacking+p.setting_up)/(3*p.total)),`Laden ${p.downloading}/${p.total}, Entpacken ${p.unpacking}/${p.total}, Einrichten ${p.setting_up}/${p.total}`,(u.started||0)*1000);else progSet("updprog",u.state==="running",0,"",(u.started||0)*1000);let msg="";if(!u.helper_installed)msg="Update-Helfer ist auf der Box nicht installiert.";else if(u.state==="running")msg=(UPD_LABEL[u.mode]||"Aktion")+" läuft…";else if(u.state==="rebooting")msg="Die Box startet neu. Diese Seite lädt nach dem Start wieder.";else if(u.reboot_pending)msg="Neustart nötig, damit der neue Kernel/Netzwerkteile aktiv werden.";else if(u.message)msg=u.message;if(u.streaming)msg="Übertragung läuft: Installieren und Neustart sind gesperrt. "+msg;$("updmsg").textContent=msg;$("updmsg").style.color=(u.state==="failed"||u.state==="refused")?"var(--crit)":"";$("u_check").disabled=$("u_dry").disabled=busy||!u.helper_installed;$("u_run").disabled=busy||u.streaming||!u.helper_installed;$("u_run").className=u.available>0?"warnbtn":"sec";$("u_reboot").disabled=busy||u.streaming||!u.helper_installed;$("u_reboot").className=u.reboot_pending?"warnbtn":"sec";if(u.log!==undefined&&$("upddet").open)$("updlog").textContent=u.log||"(leer)";const attention=busy||u.reboot_pending||u.state==="failed"||u.state==="refused"||(u.available>0);let badge="nicht geprüft",cls="ok";if(busy){badge=u.state==="rebooting"?"Neustart…":"läuft…";cls="attn";}
else if(u.reboot_pending){badge="Neustart nötig";cls="attn";}
else if(u.state==="failed"||u.state==="refused"){badge="Fehler";cls="attn";}
else if(u.available>0){badge=`${u.available} Updates verfügbar`;cls="attn";}
else if(u.last_check||u.state==="done"){badge="aktuell";cls="ok";}
$("updbadge").textContent=badge;$("updbadge").className="badge "+cls;if(attention&&window._updAttn!==true)$("updcard").open=true;window._updAttn=attention;}
async function updPost(mode,confirm){const r=await fetch("/api/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mode,confirm})});if(!r.ok)alert((await r.json()).error);tick();}
$("u_check"). installieren?\n\n• Es darf KEINE Übertragung laufen.\n• Strom nicht trennen: bei einem Kernel-Update kann die Box sonst unbrauchbar werden.\n• Danach ist meist ein Neustart nötig, und die Kameras müssen neu geprüft werden.\n• Ein automatisches Zurückrollen gibt es nicht."))updPost("run",true);};$("u_reboot"). jetzt neu starten?"))updPost("reboot",true);};$("upddet").addEventListener("toggle",tick);const DJI_STATE={idle:["Getrennt",0],searching:["Sucht",1],connecting:["Verbindet",2],pairing:["Koppelt",3],preparing:["Bereitet vor",4],wifi:["WLAN wird übergeben",5],configuring:["Stellt ein",6],starting:["Stream startet",6],streaming:["Streamt",0],status:["Nur Akku",0],stopping:["Beendet",0],error:["Fehler",0]};const DJI_BUSY=["searching","connecting","pairing","preparing","wifi","configuring","starting","stopping"];const DJI_STAB=[["off","Aus"],["rocksteady","RockSteady"],["rocksteadyplus","RockSteady+"],["horizonbalancing","HorizonBalancing"],["horizonsteady","HorizonSteady"]];let djiData={available:false,cameras:[],wifi_options:[],scan:[]},djiStruct="",djiRecv=Date.now(),djiOpen={},djiPend={};try{djiOpen=JSON.parse(localStorage.getItem("pbDjiOpen")||"{}")||{};}catch(e){djiOpen={};}
const djiName=c=>((camsByKey[c.rtmp_key]||{}).name)||c.name;const djiIsOther=c=>(djiData.wifi_options||[]).some(o=>o.ifname===c.wifi_ifname&&(o.type==="other"||o.secret_missing));const djiNeedsNet=c=>c.wifi_ifname==="manual"||djiIsOther(c);function djiCardHtml(c){const opts=djiData.wifi_options||[],known=opts.some(o=>o.ifname===c.wifi_ifname);const netOpts=opts.map(o=>{const lb=o.type==="other"?`${o.ifname} · ${o.ip}`:(o.type==="hotspot"?`${o.ssid} (WLAN-Hotspot an ${o.ifname}, ${o.ip})`:`${o.ssid} (WLAN-Netz an ${o.ifname}, ${o.ip})`);return`<option value="${esc(o.ifname)}"${c.wifi_ifname===o.ifname?" selected":""}>${esc(lb)}</option>`;}).join("");const missing=(c.wifi_ifname&&c.wifi_ifname!=="manual"&&!known)?`<option value="${esc(c.wifi_ifname)}" selected>${esc(c.wifi_ifname)} (nicht verbunden)</option>`:"";const res=["480p","720p","1080p"].map(r=>`<option${c.resolution===r?" selected":""}>${r}</option>`).join("");const stab=DJI_STAB.map(x=>`<option value="${x[0]}"${c.stabilization===x[0]?" selected":""}>${x[1]}</option>`).join("");const saved=(c.saved||[]).map(n=>`<option value="${esc(n)}"${n===c.ssid?" selected":""}>${esc(n)}</option>`).join("");const st=djiOpen[c.addr]||{},op=k=>st[k]?" open":"";const other=djiIsOther(c);return`<details class="dcam" data-addr="${esc(c.addr)}" data-sec="cam"${op("cam")}>
<summary><span class="dname"></span><span class="dright"><span class="dstate"></span> <span class="dbatt muted"></span></span></summary>
<div class="dbody">
<div class="dsub muted">${esc(c.model||"")} <span class="ddetail"></span></div>
<div class="actions"><button type="button" class="d-connect">Verbinden</button><button type="button" class="sec d-reconnect">Neu verbinden</button><button type="button" class="sec d-disconnect">Trennen</button></div>
<div class="actions" style="justify-content:space-between;align-items:center"><label class="chk" style="margin:0"><input type="checkbox" class="d-auto"${c.autoconnect?" checked":""}> Automatisch verbinden und streamen</label><button type="button" class="del d-remove">Entfernen</button></div>
<label class="chk"><input type="checkbox" class="d-statusonly"${c.status_only?" checked":""}> Nur Akkustand lesen (Kamera sendet per HDMI)</label>
<label class="f">Name<input class="d-name" maxlength="40" value="${esc(djiName(c))}"></label>
<details class="dsec" data-sec="conn"${op("conn")}><summary>Verbindung <span class="muted dsum-conn"></span></summary>
<label class="f">Verbindung der Kamera<select class="d-wifi"><option value="">– wählen –</option>${netOpts}${missing}<option value="manual"${c.wifi_ifname==="manual"?" selected":""}>Manuell (WLAN-Name, Passwort, Adresse)</option></select></label>
<div class="ph d-lockhint" style="margin-top:4px" hidden>Die Kamera ist verbunden. Änderungen an der Verbindung gelten ab der nächsten Verbindung, die laufende bleibt bestehen. <button type="button" class="sec d-reconnect" style="margin-left:6px;padding:3px 10px">Jetzt neu verbinden</button></div>
<div class="ph" style="margin-top:4px">Ein WLAN-Hotspot oder -Netz der Box: Die Kamera bekommt Name und Passwort selbst. Jede andere Verbindung (Ethernet, USB-Router, Modem): Hier den WLAN-Namen und das Passwort eintragen, dem die Kamera beitreten soll. Die Adresse der Box kommt aus der gewählten Verbindung.</div>
<div class="d-manual dnet"${djiNeedsNet(c)?"":" hidden"}>
<div class="f d-savedrow"${(c.saved||[]).length?"":" hidden"}><span class="flh"><span>Gespeicherte Netze</span><button type="button" class="flb crit d-saveddel">Löschen</button></span><select class="d-saved"><option value="">– gespeicherte Netze –</option>${saved}</select></div>
<div class="f d-nearrow"><span class="flh"><span>Netze in der Nähe</span><button type="button" class="flb d-nearscan">Netze suchen</button></span><select class="d-nearby"><option value="">– Netz wählen –</option></select><div class="d-nearmsg" role="status"></div></div>
<label class="f">WLAN-Name der Kamera<input class="d-ssid" maxlength="32" autocomplete="off" value="${esc(c.ssid||"")}"></label>
<label class="f">WLAN-Passwort<input class="d-pw" type="password" maxlength="63" autocomplete="new-password" placeholder=""></label>
<label class="f d-iprow"${other?" hidden":""}>Adresse der Box in diesem Netz<input class="d-ip" maxlength="45" value="${esc(c.ip||"")}"></label>
</div>
</details>
<details class="dsec" data-sec="video"${op("video")}><summary>Bild und Stream <span class="muted dsum-video"></span></summary>
<div class="dgrid"><label class="f">Auflösung<select class="d-res">${res}</select></label>
<label class="f">Bildrate<select class="d-fps"><option value="25"${c.fps===25?" selected":""}>25 fps</option><option value="30"${c.fps!==25?" selected":""}>30 fps</option></select></label>
<label class="f">Bitrate (Mbit/s)<input class="d-br" type="number" min="0.5" max="16" step="0.5" value="${(c.bitrate||6000)/1000}"></label>
<label class="f">Stabilisierung<select class="d-stab">${stab}</select></label></div>
<div class="ph">Die Kamera sendet auf den Schlüssel <code>${esc(c.rtmp_key||"")}</code>. Bildrate: 25 oder 30 (60 gibt es beim DJI-Livestream nicht).</div>
</details>
</div></details>`;}
let nearAuto=0;function nearNets(){const seen=new Set(),out=[];for(const n of((typeof wifiData!=="undefined"&&wifiData&&wifiData.scan&&wifiData.scan.nets)||[]).slice().sort((a,b)=>(b.signal||0)-(a.signal||0)))
if(n.ssid&&!seen.has(n.ssid)){seen.add(n.ssid);out.push(n);}
return out;}
function nearAutoScan(){if(nearAuto&&Date.now()-nearAuto>300000)nearAuto=0;if(typeof wifiData==="undefined"||!wifiData||!$("w_if").value||nearNets().length||nearAuto||(typeof sendData!=="undefined"&&sendData&&sendData.active)||!(djiData.cameras||[]).some(djiNeedsNet))return;nearAuto=Date.now();srtlaCall("POST","/api/wifi",{action:"scan",iface:$("w_if").value}).then(()=>[3000,8000].forEach(ms=>setTimeout(wifiLoad,ms))).catch(()=>{});}
function djiNearby(card){const man=card.querySelector(".d-manual"),sel=card.querySelector(".d-nearby");if(!man||!sel||man.hidden||man.offsetParent===null)return;const nets=nearNets();const sig=JSON.stringify(nets.map(n=>[n.ssid,Math.round((n.signal||0)/10),n.security]));if(sel.dataset.sig===sig||document.activeElement===sel)return;sel.dataset.sig=sig;sel.innerHTML='<option value="">'+(nets.length?"– Netz wählen –":"– keine Netze gefunden –")+'</option>'+nets.map(n=>`<option value="${esc(n.ssid)}">${esc(n.ssid)} · ${n.security&&n.security!=="offen"?"🔒 ":""}${n.signal} %</option>`).join("");}
function djiDynamic(){const d=djiData,box=$("djicams");nearAutoScan();for(const c of(d.cameras||[])){const card=box.querySelector(`.dcam[data-addr="${c.addr}"]`);if(!card)continue;let[txt,step]=DJI_STATE[c.state]||[c.state,0],col=c.state==="streaming"||c.state==="status"?"var(--ok)":c.state==="error"?"var(--crit)":c.state==="idle"?"var(--muted)":c.state==="pairing"?"var(--warn)":"var(--accent)";if(step)txt+=` (${step}/6)`;if((c.state==="idle"||c.state==="error")&&c.publishing){txt="Stream läuft (ohne Bluetooth)";col="var(--warn)";}
const e=card.querySelector(".dstate");e.textContent=txt;e.style.color=col;if(!card.querySelector(".d-name").matches(":focus")){const nm=djiName(c);card.querySelector(".dname").textContent=nm;}
{const b=card.querySelector(".dbatt"),a=c.battery_age,stale=a!=null&&a>600;b.textContent=c.battery!=null?(c.charging?"🔌 ":"")+c.battery+" %"+(stale?" (alt)":""):"";b.style.color=c.battery==null?"":(c.battery>75?"var(--ok)":(c.battery<25?"var(--crit)":""));b.style.opacity=stale?".6":"";}
let det=c.detail||(c.state==="idle"?(c.in_range?"In Reichweite":"Zuletzt nicht gesehen"):"");const left=Math.max(0,(c.retry_in||0)-Math.floor((Date.now()-djiRecv)/1000));if(c.state==="error"&&c.retry_in&&c.autoconnect)det+=(det?" – ":"")+"neuer Versuch in "+left+" s";const dt=card.querySelector(".ddetail");dt.textContent=det?"· "+det:"";dt.style.color=c.state==="error"?"var(--crit)":"";const active=c.state!=="idle"||c.publishing;card.querySelector(".d-connect").disabled=c.state==="streaming"||c.state==="status";const so=card.querySelector(".d-statusonly");if(so!==document.activeElement)so.checked=!!c.status_only;so.disabled=!!(c.mode_locked!==undefined?c.mode_locked:c.locked);card.querySelectorAll(".dsec").forEach(x=>{x.hidden=!!c.status_only;});card.querySelector(".d-auto").closest("label").hidden=!!c.status_only;card.querySelector(".d-disconnect").disabled=!active;djiNearby(card);card.querySelector(".d-lockhint").hidden=!c.locked;const ss=card.querySelector(".d-ssid");if(ss&&ss!==document.activeElement&&!djiPend[c.addr]&&ss.value!==(c.ssid||""))ss.value=c.ssid||"";const sv=card.querySelector(".d-saved");if(sv&&sv!==document.activeElement){const names=(c.saved||[]),have=[...sv.options].slice(1).map(o=>o.value);if(JSON.stringify(names)!==JSON.stringify(have))sv.innerHTML='<option value="">– gespeicherte Netze –</option>'+names.map(n=>`<option value="${esc(n)}">${esc(n)}</option>`).join("");sv.value=names.includes(c.ssid)?c.ssid:"";card.querySelector(".d-savedrow").hidden=!names.length;}
const pw=card.querySelector(".d-pw");if(pw&&pw!==document.activeElement)pw.placeholder=(c.saved||[]).includes(c.ssid)?"Passwort gespeichert – leer lassen, um es zu behalten":"Passwort";const wo=(d.wifi_options||[]).find(o=>o.ifname===c.wifi_ifname);card.querySelector(".dsum-conn").textContent=c.wifi_ifname==="manual"?"· "+(c.ssid||"–"):(c.wifi_ifname?"· "+(wo&&wo.ssid?wo.ssid:c.wifi_ifname):"");card.querySelector(".dsum-video").textContent=`· ${c.resolution||""} ${c.fps||30} fps · ${(c.bitrate||6000)/1000} Mbit/s`;const au=card.querySelector(".d-auto");if(au!==document.activeElement)au.checked=!!c.autoconnect;}}
function btRender(d){const el=$("btcard");if(!el)return;const ad=d.available?(d.adapters||[]):[],pr=d.available?(d.adapter_problems||[]):[],dr=d.driver||{};const drTxt=d.available&&dr.message&&["working","waiting","failed","unsupported"].includes(dr.state)?dr.message:"";let h="";if(!d.available)h=`<div class="wstat bad"><span class="wdot"></span><span class="wgrow">${esc(d.reason||"Der Bluetooth-Dienst ist nicht erreichbar")}</span></div>`;else{h=ad.map(a=>`<div class="wstat${a.powered?" ok":""}"><span class="wdot"></span><span class="wgrow"><b>${esc(a.label||a.name||(a.usb_id?"Bluetooth-Stick":"Eingebauter Bluetooth-Adapter"))}</b>${devDet(a)}${dnLink(a)} · ${a.powered?"bereit":"ausgeschaltet"}</span></div>`).join("")+
pr.map(x=>`<div class="wstat bad"><span class="wdot"></span><span class="wgrow"><b>${esc(x.label||x.name)}</b> <span class="muted">(${esc(x.id)})</span>${dnLink(x)} · nicht nutzbar<div class="muted" style="margin-top:3px">${esc(x.hint)}</div></span></div>`).join("");if(!ad.length&&!pr.length)h='<div class="wstat bad"><span class="wdot"></span><span class="wgrow">Kein Bluetooth-Adapter gefunden. Für DJI-Kameras einen USB-Bluetooth-Stick einstecken.</span></div>';if(drTxt)h+=`<div class="ph"${dr.state==="failed"||dr.state==="unsupported"?' style="color:var(--crit)"':""}>${esc(drTxt)}</div>`;}
setHtml(el,h);}
10BC8
let btdrvTimer=0;function btdrvRender(d){const msg=$("btdrvmsg"),go=$("btdrv_go");if(!msg||!go)return;const run=d.state==="working",ch=d.chip||null;progSet("btdrvprog",run,d.steps?Math.round(100*(d.step||0)/d.steps):0,d.step_text||"");go.disabled=!d.helper_installed||run||!!(ch&&ch.state==="working");const done=!!d.helper_installed&&!run&&(d.state==="ok"||(!!ch&&ch.state==="ok"&&(d.state==="none"||!d.state)));go.className=done?"sec":"";let bad=d.state==="failed"||d.state==="unsupported",show;if(!d.helper_installed)show="Der Treiber-Helfer ist auf dieser Box noch nicht installiert (install.sh erneut ausführen).";else if(ch&&(d.state==="none"||!d.state)){bad=ch.state==="failed"||ch.state==="unsupported";show=(d.manual||ch.manual||ch.state!=="ok")&&ch.message;}
else show=(d.manual||bad||run||d.state==="waiting")&&d.message;msg.textContent=show||"";msg.style.color=bad||!d.helper_installed?"var(--crit)":"";clearTimeout(btdrvTimer);btdrvTimer=setTimeout(btdrvLoad,run?1500:15000);}
async function btdrvLoad(){try{const r=await fetch("/api/btdriver");if(r.ok)btdrvRender(await r.json());}catch(e){clearTimeout(btdrvTimer);btdrvTimer=setTimeout(btdrvLoad,3000);}}
$("btdrv_go"). r=await fetch("/api/btdriver",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});const j=await r.json().catch(()=>({}));if(!r.ok){$("btdrvmsg").textContent=j.error||"Fehler";$("btdrvmsg").style.color="var(--crit)";$("btdrv_go").disabled=false;return;}
$("btdrvmsg").textContent="Wird gestartet …";$("btdrvmsg").style.color="";clearTimeout(btdrvTimer);btdrvTimer=setTimeout(btdrvLoad,1500);}catch(e){$("btdrvmsg").textContent="Keine Verbindung zur Box";$("btdrv_go").disabled=false;}};btdrvLoad();function phoneAge(s){return s==null?"":s<90?"gerade eben":s<5400?`vor ${Math.round(s/60)} Min.`:`vor ${Math.round(s/3600)} Std.`;}
function phoneRender(d){const el=$("phonecard");if(!el)return;const list=d.available?(d.phones||[]):[],pr=d.phone_pairing||{};setHtml(el,list.length?list.map(p=>{const lvl=p.percent==null?"":p.percent>75?" ok":p.percent<25?" bad":"";const val=p.percent==null?(p.reading?"wird gelesen …":"noch kein Wert"):`${p.percent} %`;return`<div class="wstat${lvl}" data-addr="${esc(p.addr)}"><span class="wdot"></span><span class="wgrow"><b>${esc(p.name)}</b> · ${esc(val)}${p.age!=null?`<span class="muted">· ${esc(phoneAge(p.age))}</span>`:""}`+`${p.error?`<div class="muted"style="margin-top:3px;color:var(--crit)">${esc(p.error)}</div>`:""}</span>`+`<button type="button" class="ph-read"${p.reading?" disabled":""}>Jetzt lesen</button> <button type="button" class="ph-del">Entfernen</button></div>`;}).join(""):'<div class="muted" style="margin:6px 0">Noch kein Handy gekoppelt.</div>');const b=$("ph_pair");b.disabled=!d.available;b.textContent=pr.active?"Abbrechen":"Handy koppeln";const m=$("ph_msg");m.textContent=pr.active?`Die Box ist jetzt noch ${pr.left} Sekunden sichtbar. Am Handy in den Bluetooth-Einstellungen nach neuen Geräten suchen und „belabox“ auswählen.`:(pr.message||"");}
$("ph_pair"). on=(djiData.phone_pairing||{}).active;if(await djiCmd({cmd:on?"phone_pair_stop":"phone_pair"}))djiLoad();};$("phonecard").addEventListener("click",async e=>{const b=e.target.closest("button"),row=e.target.closest("[data-addr]");if(!b||!row)return;const addr=row.dataset.addr;if(b.classList.contains("ph-read")){if(await djiCmd({cmd:"phone_read",addr}))djiLoad();}
else if(b.classList.contains("ph-del")){if(confirm("Dieses Handy entfernen? Die Kopplung wird aufgehoben, zum erneuten Abfragen muss es neu gekoppelt werden.")&&await djiCmd({cmd:"phone_remove",addr}))djiLoad();}});function ctrlRender(d){const el=$("ctrlcard");if(!el)return;const ok=d.available,list=ok?(d.controllers||[]):[],sc=d.ctrl_scan||{};setHtml(el,list.length?list.map(c=>`<div class="wstat${c.connected?" ok":""}" data-addr="${esc(c.addr)}"><span class="wdot"></span><span class="wgrow"><b>${esc(c.name)}</b> · ${c.pairing?"Koppeln …":c.connected?"verbunden":"getrennt"}`+`${c.battery!=null?`<span class="muted">· Akku ${esc(c.battery)}%</span>`:""}</span><button type="button" class="ct-del">Entfernen</button></div>`).join(""):'<div class="muted" style="margin:6px 0">Noch kein Controller gekoppelt.</div>');const all=$("ct_all").checked,found=(d.ctrl_found||[]).filter(r=>all||r.input);const f=$("ctrlfound");setHtml(f,found.map(r=>`<div class="dscan" data-addr="${esc(r.addr)}"><div>${esc(r.name||"(ohne Name)")} <span class="muted">${esc(r.addr)} · ${esc(r.rssi)} dBm</span></div><button type="button" class="ct-pair"${(d.controllers||[]).some(c=>c.pairing)?" disabled":""}>Koppeln</button></div>`).join(""));const b=$("ct_scan");b.disabled=!ok||!!sc.active;b.textContent=sc.active?"Suche läuft …":"Nach Controllern suchen";const m=$("ct_msg");m.textContent=d.ctrl_message||sc.message||(!sc.active&&(d.ctrl_found||[]).length&&!found.length?"Kein Controller erkannt. Mit „Alle Geräte zeigen“ siehst du alle Geräte in der Nähe.":"");m.style.color=d.ctrl_message?"var(--crit)":"";}
$("ct_scan"). djiCmd({cmd:"ctrl_scan"}))djiLoad();};$("ct_all"). e=>{const b=e.target.closest(".ct-del"),row=e.target.closest("[data-addr]");if(!b||!row)return;if(confirm("Diesen Controller entfernen? Die Kopplung wird aufgehoben.")&&await djiCmd({cmd:"ctrl_remove",addr:row.dataset.addr}))djiLoad();});$("ctrlfound").addEventListener("click",async e=>{const b=e.target.closest(".ct-pair"),row=e.target.closest("[data-addr]");if(!b||!row)return;if(await djiCmd({cmd:"ctrl_pair",addr:row.dataset.addr}))djiLoad();});const djiAway=c=>!(c.publishing||["streaming","status","starting","configuring","pairing"].includes(c.state));const djiSorted=l=>(l||[]).map((c,i)=>[djiAway(c)?1:0,i,c]).sort((x,y)=>x[0]-y[0]||x[1]-y[1]).map(x=>x[2]);function djiRender(d){djiData=d;djiRecv=Date.now();btRender(d);phoneRender(d);ctrlRender(d);const note=$("djinote"),box=$("djicams");if(!d.available){note.textContent=d.reason||"Bluetooth nicht verfügbar";note.style.color="var(--crit)";$("d_scan").disabled=true;box.innerHTML="";djiStruct="";$("djiscan").innerHTML="";$("djiadapt").textContent="";return;}
note.textContent=d.bleak===false?"Die Bluetooth-Bibliothek (bleak) ist nicht installiert. install.sh erneut ausführen (braucht Internet).":(d.scan_error||"");note.style.color=note.textContent?"var(--crit)":"";{const pr=(d.adapter_problems||[]).map(x=>x.hint),ad=d.adapters||[];const el=$("djiadapt");const dr=d.driver||{},drTxt=dr.message&&["working","waiting","failed","unsupported"].includes(dr.state)?dr.message:"";const bad=pr.length||!ad.length||dr.state==="failed"||dr.state==="unsupported";if(pr.length&&!drTxt){el.textContent=pr.join(" ");}
else if(pr.length){el.textContent=drTxt;}
else{el.textContent=(ad.length?"":"Kein Bluetooth-Adapter gefunden. ")+drTxt;}
el.style.color=bad?"var(--crit)":"";}
$("d_scan").disabled=!!d.scanning;$("d_scan").textContent=d.scanning?"Suche läuft …":"Nach DJI-Kameras suchen";const cams=djiSorted(d.cameras);const sig=JSON.stringify([cams.map(c=>c.addr+"|"+(c.saved||[]).join(",")+"|"+c.wifi_ifname+"|"+(c.locked?1:0)+"|"+(djiAway(c)?1:0)),(d.wifi_options||[]).map(o=>[o.ifname,o.ssid,o.type,o.ip,o.secret_missing])]);const ae=document.activeElement,typing=box.contains(ae)&&(ae.tagName==="TEXTAREA"||ae.tagName==="SELECT"||(ae.tagName==="INPUT"&&!/^(button|checkbox|radio|submit|reset)$/.test(ae.type)));if(sig!==djiStruct&&!typing){djiStruct=sig;box.innerHTML=cams.length?cams.map(djiCardHtml).join(""):'<div class="muted" style="margin-top:8px">Noch keine DJI-Kamera eingerichtet. Kamera einschalten, Bluetooth aktivieren, nahe an die Box legen und suchen.</div>';}
djiDynamic();$("djiscan").innerHTML=(d.scan||[]).map(r=>`<div class="dscan"><div>${esc(r.name)} <span class="muted">${esc(r.model)} · ${esc(r.addr)} · ${esc(r.rssi)} dBm</span></div>${r.paired?'<span class="muted">hinzugefügt</span>':`<button type="button"class="d-add"data-addr="${esc(r.addr)}"data-name="${esc(r.name)}"data-model="${esc(r.model)}"data-kind="${esc(r.kind)}">Hinzufügen</button>`}</div>`).join("");}
let djiBusyLoad=false;async function djiLoad(){if(djiBusyLoad)return;djiBusyLoad=true;try{const r=await fetch("/api/dji");if(r.ok)djiRender(await r.json());}catch(e){}djiBusyLoad=false;}
async function djiCmd(body){try{const r=await fetch("/api/dji/cmd",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const j=await r.json().catch(()=>({}));if(!r.ok){$("djierr").textContent=j.error||"Fehler";return false;}$("djierr").textContent="";return true;}catch(e){$("djierr").textContent="Keine Verbindung zur Box";return false;}}
function djiUpdate(addr,ch){const p=djiPend[addr]||(djiPend[addr]={ch:{}});Object.assign(p.ch,ch);clearTimeout(p.t);p.t=setTimeout(async()=>{const c=p.ch;delete djiPend[addr];if(await djiCmd(Object.assign({cmd:"update",addr},c)))tick();djiLoad();},400);}
setInterval(()=>{if(!document.hidden)djiDynamic();},1000);(function djiFastLoop(){const busy=djiData.available&&((djiData.scanning)||(djiData.phone_pairing||{}).active||(djiData.phones||[]).some(p=>p.reading)||(djiData.ctrl_scan||{}).active||(djiData.controllers||[]).some(c=>c.pairing)||(djiData.cameras||[]).some(c=>DJI_BUSY.includes(c.state)));Promise.resolve(busy&&!document.hidden?djiLoad():null).finally(()=>setTimeout(djiFastLoop,1000));})();$("djicams").addEventListener("toggle",e=>{const d=e.target,card=d.closest&&d.closest(".dcam");if(!card||!d.dataset||!d.dataset.sec)return;(djiOpen[card.dataset.addr]=djiOpen[card.dataset.addr]||{})[d.dataset.sec]=d.open;try{localStorage.setItem("pbDjiOpen",JSON.stringify(djiOpen));}catch(x){}},true);const djiAddr=e=>{const c=e.target.closest&&e.target.closest(".dcam");return c?c.dataset.addr:null;};$("djicams").addEventListener("change",e=>{const addr=djiAddr(e);if(!addr)return;const t=e.target,card=t.closest(".dcam"),cl=t.classList;if(cl.contains("d-wifi")){const other=(djiData.wifi_options||[]).some(o=>o.ifname===t.value&&(o.type==="other"||o.secret_missing));card.querySelector(".d-manual").hidden=!(t.value==="manual"||other);card.querySelector(".d-iprow").hidden=other;djiUpdate(addr,{wifi_ifname:t.value});}else if(cl.contains("d-nearby")){if(t.value){const cam=(djiData.cameras||[]).find(x=>x.addr===addr)||{};card.querySelector(".d-ssid").value=t.value;card.querySelector(".d-pw").value="";if((cam.saved||[]).includes(t.value))djiCmd({cmd:"use_saved",addr,ssid:t.value}).then(()=>djiLoad());else djiUpdate(addr,{ssid:t.value,password:""});t.value="";card.querySelector(".d-pw").focus();}}else if(cl.contains("d-saved")){if(t.value){card.querySelector(".d-ssid").value=t.value;card.querySelector(".d-pw").value="";djiCmd({cmd:"use_saved",addr,ssid:t.value}).then(()=>djiLoad());}}
else if(cl.contains("d-ssid"))djiUpdate(addr,{ssid:t.value});else if(cl.contains("d-pw"))djiUpdate(addr,{password:t.value});else if(cl.contains("d-ip"))djiUpdate(addr,{ip:t.value});else if(cl.contains("d-res"))djiUpdate(addr,{resolution:t.value});else if(cl.contains("d-fps"))djiUpdate(addr,{fps:parseInt(t.value)});else if(cl.contains("d-stab"))djiUpdate(addr,{stabilization:t.value});else if(cl.contains("d-statusonly"))djiCmd({cmd:"update",addr,status_only:t.checked}).then(()=>djiLoad());else if(cl.contains("d-auto"))djiCmd({cmd:"update",addr,autoconnect:t.checked}).then(()=>djiLoad());});$("djicams").addEventListener("input",e=>{const addr=djiAddr(e);if(!addr)return;const t=e.target,cl=t.classList;if(cl.contains("d-br")){const v=parseFloat(t.value);if(v>=0.5&&v<=16)djiUpdate(addr,{bitrate:Math.round(v*1000)});}
else if(cl.contains("d-name")){if(t.value.trim())djiUpdate(addr,{name:t.value.trim()});}
else if(cl.contains("d-ssid")||cl.contains("d-pw")||cl.contains("d-ip")){const cam=(djiData.cameras||[]).find(x=>x.addr===addr)||{};if(cam.locked)return;djiUpdate(addr,cl.contains("d-ssid")?{ssid:t.value}:(cl.contains("d-pw")?{password:t.value}:{ip:t.value}));}});$("djicams").addEventListener("click",async e=>{const addr=djiAddr(e);if(!addr)return;const cl=e.target.classList;if(cl.contains("d-remove")){if(confirm("Diese Kamera entfernen? Sie verschwindet auch aus der Kameraliste.")&&await djiCmd({cmd:"remove",addr})){try{if(document.activeElement&&$("djicams").contains(document.activeElement))document.activeElement.blur();}catch(x){}
djiStruct="";tick();djiLoad();}}
else if(cl.contains("d-saveddel")){const sel=e.target.closest(".d-savedrow").querySelector(".d-saved");if(sel.value&&confirm("Dieses gespeicherte Netz löschen?"))await djiCmd({cmd:"delete_saved",addr,ssid:sel.value});}
else if(cl.contains("d-nearscan")){const row=e.target.closest(".d-nearrow"),msg=row.querySelector(".d-nearmsg");nearAuto=Date.now();row.querySelector(".d-nearby").dataset.sig="";if(!$("w_if").value){msg.textContent="Keine WLAN-Karte gefunden. Name und Passwort lassen sich auch von Hand eintragen.";return;}
msg.textContent="Suche läuft …";const err=await wifiDo({action:"scan",iface:$("w_if").value},"Suche läuft …");if(err){msg.textContent=err;return;}
setTimeout(async()=>{await wifiLoad();msg.textContent=nearNets().length?"":"Keine Netze gefunden. Steckt ein WLAN-Stick, und läuft keine Sendung?";},9000);return;}
else if(cl.contains("d-connect"))await djiCmd({cmd:"connect",addr});else if(cl.contains("d-reconnect"))await djiCmd({cmd:"reconnect",addr});else if(cl.contains("d-disconnect"))await djiCmd({cmd:"disconnect",addr});else return;djiLoad();});$("djiscan").addEventListener("click",async e=>{const b=e.target.closest&&e.target.closest(".d-add");if(!b)return;if(await djiCmd({cmd:"add",addr:b.dataset.addr,name:b.dataset.name,model:b.dataset.model,kind:b.dataset.kind})){djiStruct="";tick();}djiLoad();});$("d_scan"). djiCmd({cmd:"scan"});djiLoad();};let hdmiData=null,hdmiDirty=false,hdmiBusy=false;function hdmiIsUsb(d){return!!(d&&d.settings&&d.settings.source==="usb");}
function hdmiSignalText(d){if(!d.service)return d.message||"";if(hdmiIsUsb(d)){const u=d.usb||{};if(!u.present)return u.power&&d.message?d.message:"Keine USB-Kamera angeschlossen";return[u.name,u.format].filter(Boolean).join(" · ")+(u.format&&!u.audio&&(d.settings||{}).audio!=="none"?" · kein Ton der Kamera gefunden":"");}
if(d.available===false)return"Diese Box hat keinen HDMI-Eingang";if(d.signal_known===false)return"";const s=d.signal||{};return s.locked?`Signal ${s.width}×${s.height} · ${Math.round(s.fps)} Hz`:"Kein HDMI-Signal";}
function hdmiStateText(d){if(!d.service)return"Dienst fehlt";if(d.available===false)return"kein HDMI-Eingang";return({off:"aus",waiting:hdmiIsUsb(d)?"wartet auf Kamera":"wartet auf Signal",starting:"startet …",streaming:"sendet",error:"Fehler"})[d.state]||"";}
function hdmiButtons(){const usable=!!(hdmiData&&hdmiData.service&&hdmiData.available!==false)&&!hdmiBusy;["hdmi_on","hdmi_source","hdmi_fps","hdmi_br","hdmi_audio","hdmi_fmt","hdmi_save"].forEach(id=>{$(id).disabled=!usable;});if(hdmiIsUsb(hdmiData))$("hdmi_fps").disabled=true;}
function hdmiFill(d){hdmiData=d;const st=hdmiStateText(d),e=$("hdmi_state");e.textContent=st;e.style.color=d.state==="streaming"?"var(--ok)":(d.state==="error"||!d.service?"var(--crit)":"");$("hdmi_signal").textContent=hdmiSignalText(d);const s=d.settings||{},usb=hdmiIsUsb(d);$("hdmi_name").textContent=usb?"USB-Webcam":"HDMI-Eingang";$("hdmi_audio").options[0].textContent=usb?"Ton der Kamera":"HDMI-Ton";$("hdmi_vsum").textContent=`${usb?"":s.fps+" fps · "}${(s.bitrate/1000).toFixed(1).replace(/\.0$/,"")} Mbit/s · ${s.audio==="none"?"ohne Ton":(usb?"Ton der Kamera":"HDMI-Ton")}`;if(!hdmiDirty){$("hdmi_source").value=s.source||"hdmi";$("hdmi_on").checked=!!s.enabled;$("hdmi_fps").value=String(s.fps);$("hdmi_br").value=String(s.bitrate/1000);$("hdmi_audio").value=s.audio;$("hdmi_fmt").value=s.usb_format||"auto";}
$("hdmi_fmtrow").hidden=!usb;const err=$("hdmi_err");if(d.state==="error"&&d.message){err.textContent=d.message;err.style.color="";}else if(err.dataset.keep!=="1")err.textContent="";hdmiButtons();}
async function hdmiLoad(){let d;try{d=await srtlaCall("GET","/api/hdmi");}catch(e){return;}
hdmiFill(d);}
async function hdmiPost(body,done){const err=$("hdmi_err");err.dataset.keep="";err.textContent="";hdmiBusy=true;hdmiButtons();try{const d=await srtlaCall("POST","/api/hdmi",body);hdmiDirty=false;hdmiFill(d);if(done){err.textContent=done;err.style.color="var(--ok)";err.dataset.keep="1";}tick();}
catch(x){err.textContent=x.message;err.style.color="";err.dataset.keep="1";}
hdmiBusy=false;hdmiButtons();}
$("hdmi_on").addEventListener("change",()=>hdmiPost({enabled:$("hdmi_on").checked}));$("hdmi_source").addEventListener("change",()=>hdmiPost({source:$("hdmi_source").value}));$("hdmi_save").addEventListener("click",()=>hdmiPost({bitrate:Math.round(parseFloat($("hdmi_br").value)*1000),fps:parseInt($("hdmi_fps").value,10),audio:$("hdmi_audio").value,usb_format:$("hdmi_fmt").value},"Gespeichert."));["input","change"].forEach(ev=>$("hdmi_vsec").addEventListener(ev,()=>{hdmiDirty=true;}));$("hdmi_cam").addEventListener("toggle",()=>{if($("hdmi_cam").open)hdmiLoad();});hdmiLoad();setInterval(()=>{if(due("hdmi","c_cams",20000))hdmiLoad();},3000);let netBusy=false;async function netRender(){const r=await fetch("/api/network");if(!r.ok)return;const d=await r.json();netOpts=d.options||[];const sel=$("netsel");if(document.activeElement===sel||netBusy)return;sel.innerHTML=d.options.map(o=>`<option value="${esc(o.iface)}"${(d.selected?o.iface===d.selected:o.ip===d.ip)?" selected":""}>${esc(o.label)} · ${esc(o.cam_ip||o.ip)}${o.cam_ip&&o.cam_ip!==o.ip?" (feste Adresse)":""}</option>`).join("")||"<option>keine Netzwerke</option>";const keyHint="<Schlüssel>";$("netinfo").textContent=d.selected?`Adresse für die Kamera: rtmp://${d.ip}:1935/publish/${keyHint}`:`Adresse für die Kamera: rtmp://${d.ip}:1935/publish/${keyHint} (Standardweg, noch nicht gewählt)`;}
$("netsel").addEventListener("change",async e=>{netBusy=true;const r=await fetch("/api/network",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({iface:e.target.value})});netBusy=false;if(!r.ok)$("djierr").textContent=(await r.json()).error;netRender();tick();});let srtlaEdit=null,srtlaData=null;async function srtlaCall(method,url,body){const hdr={"Content-Type":"application/json"};if(url.indexOf("/api/chat/")===0||url.indexOf("/api/twitch/log")===0)hdr["x-pb-owner"]=pbOwnerKey();if(url==="/api/twitch/login"&&body&&body.action==="start")body.owner=pbOwnerKey();const r=await fetch(url,{method,headers:hdr,body:body?JSON.stringify(body):undefined});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||"Fehler");return j;}
async function uaLoad(){try{const r=await fetch("/api/uiaccess");if(r.ok)uaRender(await r.json());}catch(e){}
try{const r=await fetch("/api/previewaccess");if(r.ok)paRender(await r.json());}catch(e){}}
function paRender(d){const c=$("pa_ext");if(c!==document.activeElement)c.checked=!!d.external;if(window.pbPrevSync)pbPrevSync(false);}
$("pa_help_btn").addEventListener("click",()=>{const h=$("pa_help");h.hidden=!h.hidden;$("pa_help_btn").setAttribute("aria-expanded",String(!h.hidden));});$("pa_ext").addEventListener("change",async e=>{$("pa_err").textContent="";try{paRender(await srtlaCall("POST","/api/previewaccess",{external:e.target.checked}));if(window.pbPrevSync)pbPrevSync(true);}
catch(x){$("pa_err").textContent=x.message;e.target.checked=!e.target.checked;if(window.pbPrevSync)pbPrevSync(false);}});function uaRender(d){const c=$("ua_block");if(c!==document.activeElement)c.checked=!!d.block_client_wifi;const l=d.client_wifi||[];$("ua_info").textContent=l.length?`Gast-WLAN der Box: ${l.map(x=>x.iface+" ("+x.ip+")").join(", ")}`:"Die Box ist gerade in keinem Gast-WLAN.";}
$("ua_block").addEventListener("change",async e=>{$("ua_err").textContent="";try{uaRender(await srtlaCall("POST","/api/uiaccess",{block_client_wifi:e.target.checked}));}
catch(x){$("ua_err").textContent=x.message;e.target.checked=!e.target.checked;}});let lastNet=null;let lastAlerts=[],healthSig="";const dismissedAlerts=new Set();const USB_LABEL={wlan:"USB-WLAN-Adapter getrennt",bt:"USB-Bluetooth-Adapter getrennt",net:"USB-Router getrennt",cam:"USB-Kamera getrennt",other:"USB-Gerät getrennt"};function alertWhen(t){const d=new Date((t||0)*1000),hm=String(d.getHours()).padStart(2,"0")+":"+String(d.getMinutes()).padStart(2,"0"),today=d.toDateString()===new Date().toDateString();return today?`um ${hm}`:`gestern um ${hm}`;}
function alertParts(a){if(a.kind==="usb"){const power=a.why==="emi"||a.why==="over";const nm=a.name?` <b translate="no">${esc(a.name)}</b>`:"";const where=(a.port||a.ids)?` <span class="muted" translate="no">(${esc([a.port?"USB "+a.port:"",a.ids].filter(Boolean).join(", "))})</span>`:"";return{main:`<span>${USB_LABEL[a.cat]||USB_LABEL.other}</span>${nm} · <span>${alertWhen(a.t)}</span>`+(a.back?` · <span>wieder da</span>`:"")+(power?` · <span>Bitte Stromversorgung prüfen</span>`:""),info:(power?"Der USB-Anschluss wurde abgeschaltet (Störung oder Spannungseinbruch). Netzteil und USB-Hub prüfen, den Adapter möglichst direkt am Board anstecken.":"Das USB-Gerät wurde getrennt, abgezogen oder ausgefallen. Kabel und Stromversorgung prüfen.")+where};}
if(a.kind==="cam"){const dji=a.cam==="dji";return{main:`<span>${dji?"DJI-Kamera ausgefallen":"Kamera ausgefallen"}</span> <b translate="no">${esc(a.name||"")}</b> · <span>${alertWhen(a.t)}</span>`,info:dji?"Bluetooth getrennt, Akku leer oder Kamera ausgeschaltet. Kamera aufwecken und die Verbindung prüfen.":"Die Kamera sendet nicht mehr. Akku, WLAN-Verbindung und Kabel prüfen."};}
if(a.kind==="hdmi"&&a.src==="usb")return{main:`<span>USB-Kamera: nicht gefunden</span> · <span>${alertWhen(a.t)}</span>`,info:"Kamera anstecken und einschalten: Sie muss sich als USB-Webcam (UVC) melden, bei Action-Kameras im Webcam-Modus."};if(a.kind==="hdmi")return{main:`<span>HDMI: kein Signal</span> · <span>${alertWhen(a.t)}</span>`,info:"Kabel prüfen und die Kamera einschalten: Sie muss ein Bild ausgeben."};if(a.kind==="dstate"&&(a.names||[]).length)return{main:a.text||"",info:`<span>Wartet im Kernel:</span> <b translate="no">${esc(a.names.join(", "))}</b>`};return{main:a.text||"",info:""};}
const alertText=a=>alertParts(a).main;function alertBox(x){const p=alertParts(x);const close=(x.kind==="usb"||x.kind==="cam"||x.kind==="hdmi")?`<button type="button" class="abtn" data-akind="${x.kind}" data-at="${x.t}" aria-label="Schließen" title="Schließen">×</button>`:"";return`<div class="alert ${x.level}">${close}<span class="atx">${p.main}</span>${p.info?`<button type="button"class="ibtn"aria-expanded="false"aria-label="Hilfe anzeigen"title="Hilfe">i</button><div class="hlp">${p.info}</div>`:""}</div>`;}
function paintHealth(){const un=!!(typeof pipeDirty!=="undefined"&&(pipeDirty||pipDelayDirty));const note='<a href="#p_save" class="unsavedlnk">Noch nicht gespeicherte Änderungen in „Bild-in-Bild“ Anpassungen</a>';const shown=lastAlerts.filter(x=>!dismissedAlerts.has(x.kind+":"+x.t));const items=shown.map(a=>`<span class="${a.level}">${alertText(a)}</span>`);const hl=$("livehealth"),phone=getComputedStyle(hl).display==="none";const h=(un?[`<span class="crit">${note}</span>`].concat(items):(items.length?items:['<span class="ok">● Alles im grünen Bereich</span>'])).join(" · ");const a=shown.map(alertBox).join("")+(un&&phone?`<div class="alert crit">${note}</div>`:"");const sig=h+"|"+a;if(sig===healthSig)return;healthSig=sig;hl.innerHTML=h;$("alerts").innerHTML=a;$("alerts").querySelectorAll(".ibtn").forEach(b=>{b._hlp=[b.nextElementSibling];});}
setInterval(paintHealth,500);document.addEventListener("click",async e=>{const b=e.target.closest&&e.target.closest("button.abtn");if(!b)return;const key=b.dataset.akind+":"+b.dataset.at;dismissedAlerts.add(key);paintHealth();try{await srtlaCall("POST","/api/alerts/dismiss",{kind:b.dataset.akind,t:parseInt(b.dataset.at,10)});}catch(x){dismissedAlerts.delete(key);paintHealth();}});document.addEventListener("click",e=>{const l=e.target.closest&&e.target.closest("a.unsavedlnk");if(!l)return;e.preventDefault();const c=$("pipecard");c.open=true;const b=$("p_save");b.scrollIntoView({behavior:"smooth",block:"center"});setTimeout(()=>b.focus({preventScroll:true}),400);});let connNames={};const ifd=n=>connNames[n]?`${n} (${connNames[n]})`:n;const connRen=iface=>`<a href="#" class="connren" data-iface="${esc(iface)}" title="Verbindung benennen" aria-label="Verbindung benennen">✎</a>`;document.addEventListener("click",async e=>{const a=e.target.closest&&e.target.closest("a.connren");if(!a)return;e.preventDefault();const n=prompt(`Eigener Name für ${a.dataset.iface} (leer lassen = kein Name):`,connNames[a.dataset.iface]||"");if(n===null)return;try{await srtlaCall("POST","/api/devname",{key:"net:"+a.dataset.iface,name:n});}catch(x){alert(x.message);return;}
if(n.trim())connNames[a.dataset.iface]=n.trim().replace(/\s+/g," ");else delete connNames[a.dataset.iface];srtlaLoad();tick();});function setRates(net){lastNet=net;document.querySelectorAll("[data-rate]").forEach(e=>{const r=net[e.dataset.rate];e.textContent=r?fmt(r.tx_mbit):"–";});}
$("b_ups").addEventListener("change",async e=>{const iface=e.target.dataset&&e.target.dataset.up;if(!iface||!srtlaData)return;const st=srtlaData.settings,ups=new Set(st.uplinks);e.target.checked?ups.add(iface):ups.delete(iface);$("neterr").textContent="";try{await srtlaCall("POST","/api/srtla/settings",{uplinks:[...ups]});$("upinfo").textContent="Sendewege gespeichert. Die Änderung wirkt innerhalb weniger Sekunden, ohne Neustart.";}
catch(x){$("neterr").textContent=x.message;}
srtlaLoad();});async function srtlaLoad(){try{srtlaData=await srtlaCall("GET","/api/srtla");}catch(e){return;}
const d=srtlaData,sel=$("srv_sel");sel.innerHTML=d.servers.length?d.servers.map(s=>`<option value="${esc(s.id)}"${s.id===d.selected?" selected":""}>${esc(s.name)} · ${esc(s.host)}:${s.port}${s.has_streamid?" · Stream-ID gespeichert":""}</option>`).join(""):"<option value=\"\">Noch kein Server angelegt</option>";$("srv_edit").disabled=$("srv_del").disabled=!d.servers.length;const st=d.settings;if(document.activeElement.id!=="b_min")$("b_min").value=st.min_kbps;if(document.activeElement.id!=="b_max")$("b_max").value=st.max_kbps;if(document.activeElement.id!=="b_lat")$("b_lat").value=st.latency_ms;if(document.activeElement.id!=="b_spread")$("b_spread").value=st.spread||"best";connNames=d.conn_names||{};$("b_ups").innerHTML=(d.interfaces||[]).map(o=>`<div class="wstat${o.ip?" ok":""}"><span class="wdot"></span><span class="wgrow"><b>${esc(o.label)}</b>${connNames[o.iface]?" ("+esc(connNames[o.iface])+")":""} ${connRen(o.iface)} · ${esc(o.ip)}</span><span class="muted"><b data-rate="${esc(o.iface)}">–</b> Mbit/s ↑</span><label><input type="checkbox" data-up="${esc(o.iface)}"${st.uplinks.includes(o.iface)?" checked":""}> Zum Senden verwenden</label></div>`).join("")+(st.uplinks||[]).filter(u=>!(d.interfaces||[]).some(o=>o.iface===u)).map(u=>`<div class="wstat"><span class="wdot"></span><span class="wgrow"><b>${esc(ifd(u))}</b> ${connRen(u)} · gerade nicht verbunden</span><label><input type="checkbox" data-up="${esc(u)}" checked> Zum Senden verwenden</label></div>`).join("");if(!$("b_ups").innerHTML)$("b_ups").innerHTML='<span class="muted">keine Netzwerke gefunden</span>';if(lastNet)setRates(lastNet);const cur=d.servers.find(s=>s.id===d.selected);if(wifiData)wifiLoad();$("srtlainfo").textContent=cur?`Gewählter Server: ${cur.name} (${cur.host}:${cur.port}). `:"Legen Sie einen Server an, um ihn später zum Senden zu wählen.";}
function srtlaFormReset(){srtlaEdit=null;$("srvform").reset();$("srv_save").textContent="Server hinzufügen";$("srv_cancel").hidden=true;$("s_sid").placeholder="Stream-ID (optional)";}
$("srv_sel").addEventListener("change",async e=>{if(!e.target.value)return;try{await srtlaCall("POST","/api/srtla/select",{id:e.target.value});$("srtlaerr").textContent="";}catch(x){$("srtlaerr").textContent=x.message;}srtlaLoad();askRestart();});$("srv_edit").addEventListener("click",()=>{const s=srtlaData&&srtlaData.servers.find(x=>x.id===$("srv_sel").value);if(!s)return;srtlaEdit=s.id;$("s_name").value=s.name;$("s_host").value=s.host;$("s_port").value=s.port;$("s_sid").value="";$("s_sid").placeholder=s.has_streamid?"Stream-ID (leer lassen = behalten)":"Stream-ID (optional)";$("srv_save").textContent="Änderungen speichern";$("srv_cancel").hidden=false;$("s_name").focus();});$("srv_cancel").addEventListener("click",srtlaFormReset);$("srv_del").addEventListener("click",async()=>{const id=$("srv_sel").value;if(!id||!confirm("Diesen Server löschen?"))return;try{await srtlaCall("DELETE","/api/srtla/"+id);$("srtlaerr").textContent="";}catch(x){$("srtlaerr").textContent=x.message;}srtlaFormReset();srtlaLoad();askRestart();});$("srvform").addEventListener("submit",async e=>{e.preventDefault();$("srtlaerr").textContent="";const body={name:$("s_name").value,host:$("s_host").value,port:+$("s_port").value,streamid:$("s_sid").value};try{if(srtlaEdit)await srtlaCall("POST","/api/srtla/"+srtlaEdit,body);else await srtlaCall("POST","/api/srtla",body);srtlaFormReset();}
catch(x){$("srtlaerr").textContent=x.message;}srtlaLoad();askRestart();});async function saveSrtlaSettings(note){$("srtlaerr").textContent="";const ups=[...document.querySelectorAll("#b_ups [data-up]")].filter(c=>c.checked).map(c=>c.dataset.up);try{await srtlaCall("POST","/api/srtla/settings",{min_kbps:+$("b_min").value,max_kbps:+$("b_max").value,latency_ms:+$("b_lat").value,uplinks:ups,spread:$("b_spread").value});await srtlaLoad();$("srtlainfo").textContent=note||((sendData&&sendData.active)?"Gespeichert. Gilt erst nach einem Neustart der Sendung (Knopf oben bei Live).":"Einstellungen gespeichert. Gilt ab dem nächsten Start der Sendekette.");askRestart();}
catch(x){$("srtlaerr").textContent=x.message;}}
$("b_save").addEventListener("click",()=>saveSrtlaSettings());$("b_save_spread").addEventListener("click",()=>saveSrtlaSettings());srtlaLoad();function devDet(c){const det=[c.usb_id].filter(Boolean).join(", ");return det?` <span class="muted">(${esc(det)})</span>`:"";}
const dnLink=c=>c.key?` <a href="#" class="dn muted" data-dn="${esc(c.key)}" data-dnname="${esc(c.custom?(c.label||""):"")}" data-dnlabel="${esc(c.label||c.name||"")}" title="Eigenen Namen für dieses Gerät vergeben (leer = Standardname)">umbenennen</a>`:"";function devInfo(c){const label=c.label||c.name;if(label)return` · ${esc(label)}${devDet(c)}${dnLink(c)}`;if(c.usb_id)return` · <span class="muted">USB ${esc(c.usb_id)}</span>${dnLink(c)}`;return(c.driver?` · <span class="muted">Treiber ${esc(c.driver)}</span>`:"")+dnLink(c);}
async function devRename(a){const cur=a.dataset.dnname||a.dataset.dnlabel||"";const n=prompt("Name dieses Geräts (leer lassen = Standardname):",cur);if(n===null)return;try{await srtlaCall("POST","/api/devname",{key:a.dataset.dn,name:n});}catch(x){alert(x.message);return;}
wifiLoad();djiLoad();}
document.addEventListener("click",e=>{const a=e.target.closest&&e.target.closest("a.dn");if(a){e.preventDefault();devRename(a);}});let wifiData=null,wifiTimer=null,wifiSig=null;async function wifiLoad(){let d;try{d=await srtlaCall("GET","/api/wifi");}catch(e){return;}
wifiData=d;const sel=$("w_if"),cur=sel.value,usable=d.cards.filter(c=>!c.camera_net);if(document.activeElement!==sel){sel.innerHTML=usable.map(c=>`<option value="${esc(c.iface)}">${esc(c.iface)}${(c.label||c.name)?" · "+esc(c.label||c.name):(c.ip?" · "+esc(c.ip):"")}</option>`).join("")||'<option value="">keine WLAN-Karte</option>';if(usable.some(c=>c.iface===cur))sel.value=cur;}
const ups=((srtlaData&&srtlaData.settings)||{}).uplinks||[];renderWifiDev(d);const sig=d.cards.map(c=>c.iface+":"+c.ip).join("|");if(wifiSig!==null&&sig!==wifiSig)srtlaLoad();wifiSig=sig;const nets=(d.scan&&d.scan.nets)||[];const bars=p=>"▂▄▆█".split("").map((c,i)=>`<span style="color:${p>=(i+1)*25-12?"var(--ok)":"var(--line)"}">${c}</span>`).join("");$("wifinets").innerHTML=nets.length?`<div class="muted" style="margin-bottom:4px">Gefundene Netze von <b>${esc((d.scan&&d.scan.iface)||"")}</b>${(d.scan&&d.scan.iface&&$("w_if").value&&d.scan.iface!==$("w_if").value)?`(nicht von ${esc($("w_if").value)}:dafür „Netze suchen“ drücken)`:""} – Zeile anklicken zum Übernehmen:</div><div class="wlist">`+nets.map(n=>`<a href="#" class="wrow${n.in_use?" cur":""}" data-ssid="${esc(n.ssid)}"><span class="wname">${esc(n.ssid)}</span><span class="wsig">${bars(n.signal)} ${n.signal} %</span><span class="wsec">${n.security&&n.security!=="offen"?"🔒 "+esc(n.security):"offen"}${(d.saved||[]).includes(n.ssid)?" · gespeichert":""}</span>${n.in_use?'<span class="wuse">verbunden</span>':""}</a>`).join("")+"</div>":wifiDebug(d);$("wifisaved").innerHTML=(d.saved||[]).length?"Gespeicherte Netze (Name anklicken, Passwort leer lassen und „Verbinden“): "+d.saved.map(s=>`<a href="#" data-ssid="${esc(s)}">${esc(s)}</a> <a href="#" data-forget="${esc(s)}" class="muted">vergessen</a>`).join(" · "):"";wifiPwHint();{const dr=d.driver||{},el=$("wifidriver");el.textContent=["working","waiting","failed","unsupported"].includes(dr.state)?(dr.message||""):"";el.style.color=dr.state==="failed"||dr.state==="unsupported"?"var(--crit)":"";}
const hsAct=String(d.action||"").startsWith("hotspot_");$("wifimsg").textContent=d.helper_installed?(hsAct?"":(d.message||"")):"Der WLAN-Helfer ist auf dieser Box noch nicht installiert (install.sh erneut ausführen).";$("hsmsg").textContent=!d.helper_installed?"":(hsAct?(d.state==="working"?"Läuft …":(d.message||"")):hsMsg);for(const k of Object.keys(hsSaving)){if(d.state==="done"&&d.action==="hotspot_save"){hsOpen[k]=false;delete hsDraft[k];delete hsSaving[k];renderWifiDev(d,true);}
else if(d.state==="error"){delete hsSaving[k];}}
$("w_scan").disabled=$("w_connect").disabled=$("w_disc").disabled=!d.helper_installed||d.state==="working";if(d.state==="working"){(hsAct?$("hsmsg"):$("wifimsg")).textContent="Läuft …";clearTimeout(wifiTimer);wifiTimer=setTimeout(wifiLoad,1500);}}
const HS_CH={bg:[0,1,2,3,4,5,6,7,8,9,10,11,12,13],a:[0,36,40,44,48]};const hsOn0=c=>!!(c.hotspot&&c.hotspot.running);let hsOpen={},hsDraft={},hsPwText={},hsSaving={},hsMsg="";function devRow(c,ups){const head=`<b>${esc(c.iface)}</b>${devInfo(c)}`;if(c.camera_net)return`<div class="wstat${hsOn0(c)?" ok":""}"><span class="wdot"></span><span class="wgrow">${head} · Kameranetz${hsOn0(c)?" · Hotspot <b>"+esc(c.hotspot.ssid)+"</b> läuft · "+esc(c.ip):""} (hier nicht änderbar)</span></div>`;if(hsOn0(c))return`<div class="wstat ok"><span class="wdot"></span><span class="wgrow">${head} · Hotspot <b>${esc(c.hotspot.ssid)}</b> läuft · ${esc(c.ip)}</span></div>`;return`<div class="wstat${c.ip?" ok":""}"><span class="wdot"></span><span class="wgrow">${head} · ${c.ip?`verbunden${c.ssid?" mit <b>"+esc(c.ssid)+"</b>":""}· ${esc(c.ip)}${c.signal!=null?" · Signal "+c.signal+" %":""}`:"nicht verbunden"}</span>`+
(c.ip?`<label title="Sendet die Box auch über dieses Netz. Danach Sendekette neu starten."><input type="checkbox" data-wup="${esc(c.iface)}"${ups.includes(c.iface)?" checked":""}> Zum Senden verwenden</label>`:"")+`</div>`;}
function hsDefaults(c){const h=c.hotspot;return{ssid:h?h.ssid:"IRL4YOU-BOX",pw:"",band:h&&h.band==="a"?"a":"bg",ch:h?(h.channel||0):0};}
function hsPanel(c,busy){const dr=hsDraft[c.iface]||(hsDraft[c.iface]=hsDefaults(c)),chs=HS_CH[dr.band]||HS_CH.bg,off=busy?" disabled":"";return`<div class="hspanel" data-iface="${esc(c.iface)}"><div class="wform">
<label class="muted">Name des Hotspots<input class="hs-ssid" maxlength="32" autocomplete="off" value="${esc(dr.ssid)}"></label>
<label class="muted">Passwort (8 bis 63 Zeichen)<input class="hs-pw-in" maxlength="63" autocomplete="off" value="${esc(dr.pw)}" placeholder="${c.hotspot?"leer = gespeichertes Passwort":""}"></label>
<label class="muted">Band<select class="hs-band"><option value="bg"${dr.band==="bg"?" selected":""}>2,4 GHz (reicht weiter)</option><option value="a"${dr.band==="a"?" selected":""}${c.band5===false?" disabled":""}>5 GHz (schneller, weniger Störungen)</option></select></label>
<label class="muted">Kanal<select class="hs-ch">${chs.map(n=>`<option value="${n}"${+dr.ch===n?" selected":""}>${n?`Kanal ${n}`:"automatisch"}</option>`).join("")}</select></label>
</div><div class="actions"><button type="button" class="hs-save"${off}>Speichern</button> <button type="button" class="sec hs-gen">Passwort erzeugen</button> <button type="button" class="sec hs-close">Schließen</button></div></div>`;}
function hsControls(c,busy){if(c.ap===false)return"";const on=hsOn0(c),pw=hsPwText[c.iface],off=busy?" disabled":"";const label=`<span class="muted" title="Macht aus dem Stick ein eigenes WLAN der Box, zum Beispiel für DJI-Kameras oder ein Handy. Der Stick ist dann nicht mehr mit einem anderen WLAN verbunden.">Hotspot-Modus</span>`;const show=`<button type="button" class="sec hs-show">${pw?"Passwort ausblenden":"Passwort anzeigen"}</button>`;const pwRow=pw?`<div class="hsrow"><div class="pwshow" title="Klicken kopiert das Passwort">${esc(pw)}</div></div>`:"";if(c.camera_net)return on?`<div class="hsrow">${label} <b>An</b> ${show}</div>${pwRow}`:"";return`<div class="hsrow">${label}<button type="button" class="hs-toggle hsw${on?" on":" sec"}" role="switch" aria-checked="${on}" title="${on?"Hotspot ausschalten":"Hotspot einschalten"}"${off}>${on?"An":"Aus"}</button>`+
(on?show:`<button type="button" class="sec hs-edit"${hsOpen[c.iface]?" disabled":""}${off}>Einstellen</button>`+(c.hotspot?` <button type="button" class="sec hs-del" title="Entfernt den Hotspot dieser Karte ganz (Einstellungen und Profil), zum Beispiel um ihn auf einer anderen Karte einzurichten"${off}>Löschen</button>`:""))+`</div>`+(on?pwRow:(hsOpen[c.iface]?hsPanel(c,busy):""));}
function renderWifiDev(d,force){const ups=((srtlaData&&srtlaData.settings)||{}).uplinks||[],busy=!d.helper_installed||d.state==="working";const html=d.cards.length?d.cards.map(c=>`<div class="wcard" data-iface="${esc(c.iface)}">${devRow(c,ups)}${hsControls(c,bus
10BC8
y)}</div>`).join(""):'<div class="ph">Keine WLAN-Karte gefunden.</div>';const el=$("wifidev"),a=document.activeElement;if(!force&&a&&el.contains(a)&&/^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName))return;if(el._html!==html){el.innerHTML=html;el._html=html;}}
function hsCard(iface){return((wifiData&&wifiData.cards)||[]).find(x=>x.iface===iface);}
function setHsMsg(t){hsMsg=t;$("hsmsg").textContent=t;}
async function hsPost(body,wait){setHsMsg(wait);try{await srtlaCall("POST","/api/wifi",body);}catch(x){setHsMsg(x.message);return false;}
clearTimeout(wifiTimer);wifiTimer=setTimeout(wifiLoad,1500);[6000,14000].forEach(ms=>setTimeout(wifiLoad,ms));return true;}
async function hsToggle(iface){const c=hsCard(iface);if(!c)return;if(hsOn0(c)){delete hsPwText[iface];await hsPost({action:"hotspot_stop",iface},"Hotspot wird beendet …");return;}
if(!c.hotspot){hsOpen[iface]=true;setHsMsg("Bitte zuerst „Einstellen“: Name und Passwort festlegen, dann einschalten.");renderWifiDev(wifiData,true);return;}
const body={action:"hotspot_start",iface};if(c.ip){if(!confirm((c.ssid?`Die WLAN-Karte ${c.iface} ist gerade mit „${c.ssid}“ verbunden.`:`Die WLAN-Karte ${c.iface} ist gerade mit einem WLAN verbunden.`)+" Der Hotspot beendet diese Verbindung."+"\n\nHast du die Oberfläche über dieses WLAN geöffnet, bricht sie ab. Du erreichst sie danach über den Hotspot (Adresse meist 10.42.0.1) oder über ein anderes Netz.\n\nHotspot einschalten?"))return;body.confirm=true;}
await hsPost(body,"Hotspot wird gestartet …");}
async function hsSave(iface){const dr=hsDraft[iface];if(!dr)return;if(await hsPost({action:"hotspot_save",iface,ssid:(dr.ssid||"").trim(),password:dr.pw||"",band:dr.band,channel:parseInt(dr.ch,10)||0},"Speichere die Einstellungen …"))hsSaving[iface]=true;}
async function hsShow(iface){if(hsPwText[iface]){delete hsPwText[iface];renderWifiDev(wifiData,true);return;}
try{const r=await srtlaCall("GET","/api/wifi/hotspot?iface="+encodeURIComponent(iface));hsPwText[iface]=r.password;setHsMsg("");}
catch(x){setHsMsg(x.message);}
renderWifiDev(wifiData,true);}
$("wifidev").addEventListener("click",e=>{const t=e.target.closest&&e.target.closest("button");if(!t)return;const card=t.closest(".wcard");if(!card)return;const iface=card.dataset.iface;if(t.classList.contains("hs-toggle"))hsToggle(iface);else if(t.classList.contains("hs-edit")){hsOpen[iface]=true;hsDraft[iface]=hsDefaults(hsCard(iface)||{});setHsMsg("");renderWifiDev(wifiData,true);}
else if(t.classList.contains("hs-close")){hsOpen[iface]=false;delete hsDraft[iface];renderWifiDev(wifiData,true);}
else if(t.classList.contains("hs-del")){if(confirm(`Hotspot auf ${iface} löschen?\n\nName, Passwort und Einstellungen dieser Karte werden entfernt. Kameras, die damit verbunden waren, finden ihn dann nicht mehr.`)){delete hsPwText[iface];hsPost({action:"hotspot_delete",iface},"Hotspot wird gelöscht …");}}
else if(t.classList.contains("hs-save"))hsSave(iface);else if(t.classList.contains("hs-show"))hsShow(iface);else if(t.classList.contains("hs-gen")){const al="abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789",a=new Uint32Array(12);crypto.getRandomValues(a);const dr=hsDraft[iface]||(hsDraft[iface]=hsDefaults(hsCard(iface)||{}));dr.pw=[...a].map(n=>al[n%al.length]).join("");const inp=card.querySelector(".hs-pw-in");if(inp)inp.value=dr.pw;setHsMsg("Neues Passwort eingetragen. Mit „Speichern“ wird es übernommen; bitte notieren.");}});$("wifidev").addEventListener("input",e=>{const card=e.target.closest&&e.target.closest(".wcard");const dr=card&&hsDraft[card.dataset.iface];if(!dr)return;const c=e.target.classList;if(c.contains("hs-ssid"))dr.ssid=e.target.value;else if(c.contains("hs-pw-in"))dr.pw=e.target.value;else if(c.contains("hs-band")){dr.band=e.target.value;const sel=card.querySelector(".hs-ch"),chs=HS_CH[dr.band]||HS_CH.bg;if(!chs.includes(+dr.ch))dr.ch=0;sel.innerHTML=chs.map(n=>`<option value="${n}"${+dr.ch===n?" selected":""}>${n?`Kanal ${n}`:"automatisch"}</option>`).join("");}else if(c.contains("hs-ch"))dr.ch=parseInt(e.target.value,10)||0;});function wifiDebug(d){const dbg=(d.scan&&d.scan.debug)||[];if(!dbg.length||d.state==="working")return"";return`<details class="sens" open><summary class="muted">Technische Angaben zur Karte ${esc(d.scan.iface||"")} (die Suche hat nichts gefunden)</summary><pre>${esc(dbg.join("\n"))}</pre></details>`;}
function wifiPwHint(){const saved=((wifiData&&wifiData.saved)||[]).includes($("w_ssid").value);$("w_pw").placeholder=saved?"leer = gespeichertes Passwort verwenden":"leer bei offenem Netz";}
async function wifiDo(body,busy){$("wifimsg").textContent=busy;try{await srtlaCall("POST","/api/wifi",body);}catch(x){$("wifimsg").textContent=x.message;return x.message||"Fehler";}
clearTimeout(wifiTimer);wifiTimer=setTimeout(wifiLoad,1200);[6000,14000].forEach(ms=>setTimeout(wifiLoad,ms));return"";}
$("w_scan").addEventListener("click",()=>wifiDo({action:"scan",iface:$("w_if").value},"Suche läuft …"));$("w_connect").addEventListener("click",async()=>{const body={action:"connect",iface:$("w_if").value,ssid:$("w_ssid").value,password:$("w_pw").value,hidden:$("w_hidden").checked};await wifiDo(body,"Verbinde …");$("w_pw").value="";});$("w_disc").addEventListener("click",()=>wifiDo({action:"disconnect",iface:$("w_if").value},"Trenne …"));$("w_ssid").addEventListener("input",wifiPwHint);$("wificard").addEventListener("change",async e=>{const iface=e.target.dataset&&e.target.dataset.wup;if(!iface||!srtlaData)return;const st=srtlaData.settings,ups=new Set(st.uplinks);e.target.checked?ups.add(iface):ups.delete(iface);try{await srtlaCall("POST","/api/srtla/settings",{uplinks:[...ups]});$("wifimsg").textContent=(e.target.checked?"Zum Senden gespeichert":"Nicht mehr zum Senden")+". Die Änderung wirkt innerhalb weniger Sekunden, ohne Neustart.";}catch(x){$("wifimsg").textContent=x.message;e.target.checked=!e.target.checked;}
srtlaLoad();});$("wificard").addEventListener("click",e=>{const a=e.target.closest("a");if(!a)return;e.preventDefault();if(a.dataset.ssid!==undefined){$("w_ssid").value=a.dataset.ssid;wifiPwHint();}
if(a.dataset.forget!==undefined)wifiDo({action:"forget",ssid:a.dataset.forget},"Wird vergessen …");});wifiLoad();setInterval(()=>{if(due("wifi","netcard",60000)&&!["w_if","w_ssid","w_pw"].includes(document.activeElement.id))wifiLoad();},10000);const FREE_ID="5";let posFree={1:{x:500,y:500},2:{x:500,y:500},3:{x:500,y:500}};const STYLE_DEF=()=>({visible:true,opacity:100,crop:{l:0,r:0,t:0,b:0},radius:0,border:{enabled:false,width:6,color:"#ffffff",opacity:100}});const CROP_W=1920,CROP_H=1080,CROP_KEEP=32;let pipStyles={1:STYLE_DEF(),2:STYLE_DEF(),3:STYLE_DEF()};const styleOf=k=>pipStyles[k]||(pipStyles[k]=STYLE_DEF());const clampN=(v,lo,hi,d)=>{v=Math.round(+v);return isNaN(v)?d:Math.max(lo,Math.min(hi,v));};const hexRgba=(h,a)=>{const n=parseInt(String(h).slice(1),16)||0;return`rgba(${n>>16&255},${n>>8&255},${n&255},${a})`;};function styleBlockHtml(){const num=(f,l,min,max,step)=>`<label class="muted">${l}<input type="number" data-f="${f}" min="${min}" max="${max}" step="${step||1}"></label>`;return`<summary>Aussehen <span class="pvs-sum"></span></summary>
<label class="muted" style="display:block;margin-top:6px"><input type="checkbox" data-f="vis"> Bild einblenden</label>
<div class="sech">Ecken</div>
<div class="pvs-grid">${num("rd","Eckenrundung (Pixel)",0,60)}</div>
<div class="sech">Beschneiden</div>
<div class="pvs-info muted">Pixel, die links, rechts, oben und unten abgeschnitten werden, wie bei einem Bild von 1920 × 1080.</div>
<div class="pvs-grid">${num("cl","Links",0,1888,2)}${num("cr","Rechts",0,1888,2)}${num("ct","Oben",0,1048,2)}${num("cb","Unten",0,1048,2)}</div>
<div class="pvs-info muted" data-o="cropinfo"></div>
<div class="sech">Rahmen</div>
<label class="muted"><input type="checkbox" data-f="be"> Rahmen um dieses Bild</label>
<div class="pvs-grid" data-o="bopts">${num("bw","Dicke (Pixel)",1,40)}<label class="muted">Farbe<input type="color" data-f="bc"></label>${num("bo","Deckkraft (%)",10,100)}</div>`;}
function styleFill(k,force){const st=styleOf(k),box=document.querySelector(`.pvs[data-k="${k}"]`);if(!box)return;const put=(f,v)=>{const e=box.querySelector(`[data-f="${f}"]`);if(!e||(!force&&document.activeElement===e))return;if(e.type==="checkbox")e.checked=!!v;else e.value=v;};put("vis",st.visible);put("rd",st.radius);put("cl",st.crop.l);put("cr",st.crop.r);put("ct",st.crop.t);put("cb",st.crop.b);put("be",st.border.enabled);put("bw",st.border.width);put("bc",st.border.color);put("bo",st.border.opacity);const c=st.crop,cropped=c.l||c.r||c.t||c.b;box.querySelector('[data-o="cropinfo"]').textContent=cropped?`Bild nach dem Beschneiden: ${CROP_W-c.l-c.r} × ${CROP_H-c.t-c.b} Pixel. Der Maßstab bleibt, es wird nur abgeschnitten.`:"Nicht beschnitten.";box.querySelector('[data-o="bopts"]').style.opacity=st.border.enabled?1:.5;box.querySelectorAll('[data-o="bopts"] input').forEach(e=>e.disabled=!st.border.enabled);const parts=[st.visible?"":"ausgeblendet",cropped?"beschnitten":"",st.radius?`Ecken ${st.radius} px`:"",st.border.enabled?`Rahmen ${st.border.width} px`:""].filter(Boolean);box.querySelector(".pvs-sum").textContent=parts.length?"· "+parts.join(", "):"· Standard";}
document.querySelectorAll(".pvs").forEach(box=>{const k=+box.dataset.k;box.innerHTML=styleBlockHtml();const edit=ev=>{const el=ev.target,f=el.dataset&&el.dataset.f;if(!f)return;const st=styleOf(k);if(f==="vis")st.visible=el.checked;else if(f==="rd")st.radius=clampN(el.value,0,60,0);else if(f.length===2&&f[0]==="c"){const key=f[1],opp={l:"r",r:"l",t:"b",b:"t"}[key],total=(key==="l"||key==="r")?CROP_W:CROP_H;st.crop[key]=Math.floor(clampN(el.value,0,total-CROP_KEEP,0)/2)*2;st.crop[key]=Math.max(0,Math.min(st.crop[key],total-CROP_KEEP-st.crop[opp]));}
else if(f==="be")st.border.enabled=el.checked;else if(f==="bw")st.border.width=clampN(el.value,1,40,6);else if(f==="bc")st.border.color=/^#[0-9a-fA-F]{6}$/.test(el.value)?el.value.toLowerCase():"#ffffff";else if(f==="bo")st.border.opacity=clampN(el.value,10,100,100);styleFill(k,ev.type==="change");renderPreview();};box.addEventListener("input",edit);box.addEventListener("change",edit);styleFill(k,true);});let pipeData=null,pipeDirty=false,pipDelayDirty=false,pipLive={},pipCams=[],pipCorners=[];let pipPrevMain="";const DL=[["p_delay","n_delay","v_delay","main_delay_ms"],["p_delay1","n_delay1","v_delay1","pip_delay_ms"],["p_delay2","n_delay2","v_delay2","pip2_delay_ms"],["p_delay3","n_delay3","v_delay3","pip3_delay_ms"]];function setDelays(vals){DL.forEach(([r,n,v],i)=>{const x=Math.max(0,Math.min(3000,+vals[i]||0));const act=document.activeElement.id;if(act===r||act===n)return;$(r).max=3000;$(r).value=x;$(n).value=x;$(v).textContent=x;});}
function delayBody(){const o={};DL.forEach(([r,n,v,k])=>{o[k]=+($(n).value||0);});return o;}
const isDelayCtl=id=>DL.some(([r,n])=>id===r||id===n);const DL_ENGINE_MAX=750;function dlWarn(){const el=$("dl_warn");if(!el)return;const pip=$("p_type").value==="pip",always=!!($("p_always")&&$("p_always").checked),vals=[+$("n_delay").value||0];if(pip)[["p_pip","n_delay1"],["p_pip2","n_delay2"],["p_pip3","n_delay3"]].forEach(([s,n])=>{if($(s).value)vals.push(+$(n).value||0);});el.hidden=!(pip&&always&&Math.max(...vals)>DL_ENGINE_MAX);}
const pipOpt=(k,sel)=>`<option value="${esc(k.key)}"${k.key===sel?" selected":""}>${esc(k.name)}${pipCams.filter(x=>x.name===k.name).length>1?" ("+esc(k.key)+")":""}${k.state==="live"?" · sendet":""}</option>`;function renderPreview(){const box=$("pvbox");if(!box)return;const W=box.clientWidth||520,H=Math.round(W*9/16);box.style.height=H+"px";const m=W/60,canFree=pipCorners.length>5;const nm=k=>{const x=pipCams.find(y=>y.key===k);return x?x.name:k;};let out='<div class="pvmain">Hauptbild: '+esc(nm($("p_main").value))+'</div>';[["p_pip","p_corner",1],["p_pip2","p_corner2",2],["p_pip3","p_corner3",3]].forEach(([cs,co,k])=>{const cam=$(cs).value;if(!cam)return;const size=Math.max(1,Math.min(100,+$("p_size"+k).value||25));let pw=W*size/100;const ph=pw*9/16;const cinfo=pipCams.find(y=>y.key===cam)||{},portrait=cinfo.w>0&&cinfo.h>cinfo.w;if(portrait)pw=ph*cinfo.w/cinfo.h;const st=styleOf(k),cr=portrait?{l:0,r:0,t:0,b:0}:st.crop,cw=pw*(CROP_W-cr.l-cr.r)/CROP_W,ch=ph*(CROP_H-cr.t-cr.b)/CROP_H,sc=W/1920;const bw=st.border.enabled?Math.max(1,Math.round(st.border.width*sc)):0,rad=st.radius?Math.min(Math.round(st.radius*sc),Math.min(cw,ch)/2):0;const c=+$(co).value,f=posFree[k];let x,y;if(c===5){x=f.x/1000*(W-cw);y=f.y/1000*(H-ch);}
else if(c===4){x=(W-cw)/2;y=H-ch-m;}
else{x=(c&1)?W-cw-m:m;y=(c&2)?H-ch-m:m;}
x=Math.max(0,Math.min(W-cw,x));y=Math.max(0,Math.min(H-ch,y));out+=`<div class="pvpip pv${k}${st.visible?"":" off"}" data-k="${k}" data-co="${co}" style="left:${x}px;top:${y}px;width:${cw}px;height:${ch}px;opacity:${st.visible?1:.18};border-radius:${rad}px">`+`<div class="pvin" style="left:${-pw*cr.l/CROP_W}px;top:${-ph*cr.t/CROP_H}px;width:${pw}px;height:${ph}px"></div><b>${k}</b><span>${esc(nm(cam))}${st.visible?"":" (ausgeblendet)"}</span>`+
(bw?`<i class="pvrim" style="border:${bw}px solid ${hexRgba(st.border.color,st.border.opacity/100)};border-radius:${rad}px"></i>`:"")+`</div>`;});box.innerHTML=out;$("pvnote").textContent=canFree?"Bilder mit Maus oder Finger ziehen. Danach „Bildaufbau speichern“.":"Freies Verschieben braucht den neuen Überlagerungs-Baustein (kommt beim Software-Update).";}
(function(){const box=$("pvbox");let drag=null;box.addEventListener("pointerdown",e=>{const el=e.target.closest(".pvpip");if(!el||pipCorners.length<=5)return;e.preventDefault();box.setPointerCapture(e.pointerId);const r=el.getBoundingClientRect();drag={el,k:+el.dataset.k,co:el.dataset.co,dx:e.clientX-r.left,dy:e.clientY-r.top};el.classList.add("drag");});box.addEventListener("pointermove",e=>{if(!drag)return;const b=box.getBoundingClientRect(),W=b.width,H=b.height,w=drag.el.offsetWidth,h=drag.el.offsetHeight;const x=Math.max(0,Math.min(W-w,e.clientX-b.left-drag.dx)),y=Math.max(0,Math.min(H-h,e.clientY-b.top-drag.dy));drag.el.style.left=x+"px";drag.el.style.top=y+"px";posFree[drag.k]={x:W>w?Math.round(x/(W-w)*1000):0,y:H>h?Math.round(y/(H-h)*1000):0};});const end=()=>{if(!drag)return;drag.el.classList.remove("drag");$(drag.co).value=FREE_ID;pipeDirty=true;$("pipeerr").textContent="Nicht gespeichert: Mit „Bildaufbau speichern“ übernehmen.";drag=null;};box.addEventListener("pointerup",end);box.addEventListener("pointercancel",end);window.addEventListener("resize",()=>renderPreview());})();function pipConstrain(v){v=v||{main:$("p_main").value,pip:$("p_pip").value,pip2:$("p_pip2").value,pip3:$("p_pip3").value,c1:$("p_corner").value,c2:$("p_corner2").value,c3:$("p_corner3").value};const has=k=>pipCams.some(x=>x.key===k),free=ex=>pipCams.filter(x=>!ex.includes(x.key));const main=has(v.main)?v.main:((pipCams[0]||{}).key||"");pipPrevMain=main;const pip=(has(v.pip)&&v.pip!==main)?v.pip:((free([main])[0]||{}).key||"");const pip2=(has(v.pip2)&&v.pip2!==main&&v.pip2!==pip)?v.pip2:"";const pip3=(pip2&&has(v.pip3)&&v.pip3!==main&&v.pip3!==pip&&v.pip3!==pip2)?v.pip3:"";const fill=(id,list,val,none)=>{const el=$(id);el.innerHTML=(none?'<option value="">keins</option>':"")+(list.map(k=>pipOpt(k,val)).join("")||(none?"":'<option value="">keine Kamera</option>'));el.value=val;};fill("p_main",pipCams,main,false);fill("p_pip",free([main]),pip,false);fill("p_pip2",free([main,pip]),pip2,true);fill("p_pip3",pip2?free([main,pip,pip2]):[],pip3,true);const ids=pipCorners.map((n,i)=>String(i));const taken=(i,used)=>i!==FREE_ID&&used.includes(i);const c1=ids.includes(String(v.c1))?String(v.c1):"3";let c2=String(v.c2);if(taken(c2,[c1])||!ids.includes(c2))c2=ids.find(i=>!taken(i,[c1]))||"";$("p_corner").innerHTML=pipCorners.map((n,i)=>`<option value="${i}">${esc(n)}</option>`).join("");$("p_corner").value=c1;$("p_corner2").innerHTML=pipCorners.map((n,i)=>taken(String(i),[c1])?"":`<option value="${i}">${esc(n)}</option>`).join("");$("p_corner2").value=c2;let c3=String(v.c3);if(taken(c3,[c1,c2])||!ids.includes(c3))c3=ids.find(i=>!taken(i,[c1,c2]))||"";$("p_corner3").innerHTML=pipCorners.map((n,i)=>taken(String(i),[c1,c2])?"":`<option value="${i}">${esc(n)}</option>`).join("");$("p_corner3").value=c3;const nm=k=>{const x=pipCams.find(y=>y.key===k);return x?x.name:k;};const au=$("p_audio"),prev=au.value;au.innerHTML=[["main","Hauptbild",main],["pip","Kleines Bild 1",pip],["pip2","Kleines Bild 2",pip2],["pip3","Kleines Bild 3",pip3]].filter(x=>x[2]).map(x=>`<option value="${x[0]}">${x[1]} (${esc(nm(x[2]))})</option>`).join("");au.value=[...au.options].some(o=>o.value===prev)?prev:"main";$("p_corner2").disabled=!pip2;$("p_pip3").disabled=!pip2;$("p_corner3").disabled=!pip3;$("dl2").style.display=pip2?"":"none";$("dl3").style.display=pip3?"":"none";const nSlots=Math.min(4,Math.max(1,pipCams.length));document.querySelectorAll("#pipecard .camcols>.camcol").forEach((col,k)=>col.classList.toggle("slotoff",!(k<nSlots||(k===2&&pip2)||(k===3&&pip3))));renderPreview();}
function fcRender(d){const box=$("fc_btns");if(!box)return;const c=d.config,byKey=Object.fromEntries((d.cameras||[]).map(x=>[x.key,x])),nm=k=>((byKey[k]||{}).name||k),fc=c.facecam||"";const btn=(slot,title,sub,on,off)=>`<button type="button" class="sec fcb${on?" on":""}" data-slot="${slot}" aria-pressed="${on}"${off?" disabled":""}><b>${esc(title)}</b><span class="muted">${esc(sub)}</span></button>`;setHtml(box,[["pip","Kleines Bild 1"],["pip2","Kleines Bild 2"],["pip3","Kleines Bild 3"]].map(([s,t])=>btn(s,t,c[s]?nm(c[s]):"nicht belegt",!!fc&&c[s]===fc,!c[s])).join("")
+btn("","Keine Facecam","Der Ton folgt dem Hauptbild",!fc,false));}
async function fcRefresh(){try{fcRender(await srtlaCall("GET","/api/pipeline"));}catch(e){}}
$("fc_btns").addEventListener("click",async e=>{const b=e.target.closest("button.fcb");if(!b||b.disabled)return;const m=$("fc_msg");m.style.color="";m.textContent="Wird gesetzt …";try{const r=await srtlaCall("POST","/api/pipeline/facecam",{slot:b.dataset.slot});m.textContent=r.note||"Gespeichert. Der Ton wechselt sofort.";}
catch(x){m.style.color="var(--crit)";m.textContent=x.message;}
fcRefresh();if(typeof sendLoad==="function")setTimeout(sendLoad,600);});async function pipeLoad(){try{pipeData=await srtlaCall("GET","/api/pipeline");}catch(e){return;}
const d=pipeData,c=d.config;fcRender(d);pipCams=d.cameras;pipCorners=d.corners;pipLive=d.live||{};if(pipeDirty||pipDelayDirty){$("pipeprev").textContent=d.preview||"";return;}
if(document.activeElement.id!=="p_type")$("p_type").value=c.type;if(document.activeElement.id!=="p_auto")$("p_auto").checked=c.auto_failover!==false;if(document.activeElement.id!=="p_swap")$("p_swap").value=String(c.swap_cams||0);if(document.activeElement.id!=="p_always")$("p_always").checked=!!c.always_ready;posFree={1:{x:c.x??500,y:c.y??500},2:{x:c.x2??500,y:c.y2??500},3:{x:c.x3??500,y:c.y3??500}};[1,2,3].forEach(k=>{pipStyles[k]=Object.assign(STYLE_DEF(),(c.styles||{})[k]||{});pipStyles[k].crop=Object.assign({l:0,r:0,t:0,b:0},pipStyles[k].crop);pipStyles[k].border=Object.assign(STYLE_DEF().border,pipStyles[k].border);pipStyles[k].opacity=100;delete pipStyles[k].border.radius;styleFill(k);});pipConstrain({main:c.main,pip:c.pip,pip2:c.pip2,pip3:c.pip3,c1:c.corner,c2:c.corner2??2,c3:c.corner3??0});[["p_size1",c.size_pct],["p_size2",c.size_pct2??c.size_pct],["p_size3",c.size_pct3??c.size_pct]].forEach(([id,v])=>{if(document.activeElement.id!==id)$(id).value=v;});renderPreview();setDelays([c.main_delay_ms,c.pip_delay_ms,c.pip2_delay_ms,c.pip3_delay_ms]);dlWarn();pipLive=d.live||{};$("p_audio").value=c.audio;{const inPic=[c.main,c.pip,c.pip2,c.pip3].filter(Boolean),bar=$("swapbar");const order=(pipCams||[]).map(x=>x.key).filter(k=>inPic.includes(k)),byKey=Object.fromEntries((pipCams||[]).map(x=>[x.key,x]));bar.hidden=!(c.type==="pip"&&c.main&&inPic.length>1);setHtml($("swapbtns"),order.map(k=>{const cam=byKey[k]||{},on=k===c.main;const gone=!on&&cam.state==="offline";return`<button type="button" class="${on?"on":"swapto"}" ${on?'aria-pressed="true" disabled':(gone?`data-key="${esc(k)}"disabled`:`data-key="${esc(k)}"`)} title="${on?"Ist gerade das Hauptbild":gone?"Diese Kamera ist nicht verbunden und lässt sich nicht zum Hauptbild machen.":(seamlessTo(c.main,k)?"Als Hauptbild verwenden (tauscht mit dem aktuellen Hauptbild, ohne Unterbrechung der Sendung)":"Als Hauptbild verwenden (tauscht mit dem aktuellen Hauptbild; läuft die Sendung, startet der Encoder dafür neu, etwa 5 Sekunden ohne Bild)")}"><span class="dot${cam.state==="live"?"":" off"}"></span>${esc(cam.name||k)}</button>`;}).join(""));}
document.querySelectorAll(".piponly").forEach(e=>e.hidden=$("p_type").value!=="pip");$("pipeprev").textContent=d.preview||"(noch keine Hauptkamera gewählt)";let info=!c.main?"Noch keine Pipeline gespeichert.":c.type==="pip"?`Gespeichert: Bild-in-Bild, Hauptbild ${c.main}, kleines Bild ${[c.pip,c.pip2,c.pip3].filter(Boolean).join(", ")}.`:`Gespeichert: eine Kamera, Hauptbild ${c.main}.`;document.querySelectorAll(".pvs").forEach(e=>e.hidden=d.plugin_present&&d.plugin_style===false);if(d.needs_plugin&&!d.plugin_present)info+=" Bild-in-Bild braucht den Überlagerungs-Baustein, der noch nicht auf der Box installiert ist.";$("pipeinfo").textContent=info;}
$("p_type").addEventListener("change",()=>document.querySelectorAll(".piponly").forEach(e=>e.hidden=$("p_type").value!=="pip"));$("pipecard").addEventListener("input",e=>{const id=e.target.id||"";if(/^p_size\d$/.test(id))renderPreview();if(isDelayCtl(id)){const row=DL.find(([r,n])=>id===r||id===n),val=Math.max(0,Math.min(3000,+e.target.value||0));$(row[0]).value=val;if(id!==row[1])$(row[1]).value=val;$(row[2]).textContent=val;pipDelayDirty=true;dlWarn();return;}
pipeDirty=true;$("pipeerr").textContent="Nicht gespeichert: Mit „Bildaufbau speichern“ übernehmen.";});$("pipecard").addEventListener("change",async e=>{dlWarn();if(isDelayCtl(e.target.id||"")){const row=DL.find(([r,n])=>(e.target.id===r||e.target.id===n));const liveOk=(row[3]==="main_delay_ms"?pipLive.main:pipLive.pips);if(!pipeDirty&&liveOk){await pipSave();}else{pipeDirty=true;$("pipeerr").textContent=pipLive.main?"Nicht gespeichert: Mit „Bildaufbau speichern“ übernehmen (Sendekette startet kurz neu).":"Nicht gespeichert: Mit „Bildaufbau speichern“ übernehmen.";}
return;}
pipeDirty=true;if(e.target.id==="p_main"){const v={main:$("p_main").value,pip:$("p_pip").value,pip2:$("p_pip2").value,pip3:$("p_pip3").value,c1:$("p_corner").value,c2:$("p_corner2").value,c3:$("p_corner3").value};["pip","pip2","pip3"].forEach(k=>{if(v[k]===v.main&&pipPrevMain&&pipPrevMain!==v.main)v[k]=pipPrevMain;});pipConstrain(v);return;}
pipConstrain();});async function pipSave(){$("pipeerr").textContent="";const body={type:$("p_type").value,main:$("p_main").value,pip:$("p_pip").value,corner:+$("p_corner").value,pip2:$("p_pip2").value,corner2:+$("p_corner2").value,pip3:$("p_pip3").value,corner3:+$("p_corner3").value,x:posFree[1].x,y:posFree[1].y,x2:posFree[2].x,y2:posFree[2].y,x3:posFree[3].x,y3:posFree[3].y,size_pct:+$("p_size1").value,size_pct2:+$("p_size2").value,size_pct3:+$("p_size3").value,audio:$("p_audio").value,auto_failover:$("p_auto").checked,swap_cams:+$("p_swap").value,always_ready:$("p_always").checked,styles:pipStyles,...delayBody()};try{const r=await srtlaCall("POST","/api/pipeline",body);pipeDirty=false;pipDelayDirty=false;$("pipeerr").textContent=(r&&r.note)||"Gespeichert.";if(r&&r.restarted)pipWatchRestart((r.note||"Gespeichert."));}catch(x){$("pipeerr").textContent=x.message;}
pipeLoad();}
let pipWatchTok=0;async function pipWatchRestart(base){const tok=++pipWatchTok,t0=Date.now(),el=$("pipeerr");let seenOther=false;for(let i=0;i<120&&tok===pipWatchTok;i++){await new Promise(r=>setTimeout(r,1000));let d;try{d=await srtlaCall("GET","/api/send");}catch(e){continue;}
const sec=Math.round((Date.now()-t0)/1000);if(d.state==="running"){if(seenOther||sec>=4){el.textContent=`Gespeichert. Die Übertragung läuft wieder (nach ${sec} Sekunden).`;el.style.color="var(--ok)";setTimeout(()=>{if(tok===pipWatchTok)el.style.color="";},8000);return;}}else if(d.state==="refused"||d.state==="stopped"){el.textContent="Gespeichert, aber die Übertragung läuft nicht: "+(d.message||LIVE_LABEL[d.state]||d.state);return;}else{seenOther=true;el.textContent=base+" "+(LIVE_LABEL[d.state]||d.state)+" … ("+sec+" s)";}}}
$("p_save").addEventListener("click",pipSave);pipeLoad();setInterval(()=>{if(due("pipe","pipecard",30000)&&!["p_main","p_pip","p_type","p_corner","p_pip2","p_corner2","p_pip3","p_corner3","p_size1","p_size2","p_size3","p_auto","p_always","p_delay","n_delay","p_delay1","n_delay1","p_delay2","n_delay2","p_delay3","n_delay3","p_audio"].includes(document.activeElement.id))pipeLoad();},5000);const LIVE_LABEL={stopped:"Nicht live",starting:"Startet…",running:"LIVE",restarting:"Verbindung wird neu aufgebaut…",stopping:"Wird beendet…",refused:"Nicht gestartet"};let sendData=null,pendIgnored="";const SM_LOCKED=["srtlacard","rcard"];function setSM(on){document.body.classList.toggle("sm",on);document.querySelector("#sm_btn .smtx").textContent=on?"Streammodus: an":"Streammodus: aus";$("sm_btn").title=on?"Streammodus ist an":"Streammodus ist aus";$("sm_btn").classList.toggle("smon",on);SM_LOCKED.forEach(id=>{const c=$(id);c.classList.toggle("locked",on);if(on)c.open=false;});$("srtlalock").hidden=!on;$("rlock").hidden=!on;try{localStorage.setItem("pb_sendemodus",on?"1":"0");}catch(e){}}
SM_LOCKED.forEach(id=>{$(id).addEventListener("toggle",()=>{if($(id).classList.contains("locked")&&$(id).open)$(id).open=false;});$(id).querySelector("summary").addEventListener("click",e=>{if($(id).classList.contains("locked"))e.preventDefault();});});$("sm_btn").addEventListener("click",()=>setSM(!document.body.classList.contains("sm")));let smStart=false;try{smStart=localStorage.getItem("pb_sendemodus")==="1";}catch(e){}
async function sendLoad(){let d;try{d=await srtlaCall("GET","/api/send");}catch(e){return;}
sendData=d;updHdrLive(d);renderFoot(d);$("livestate").textContent=LIVE_LABEL[d.state]||d.state;$("livestate").style.color=d.state==="running"?"var(--ok)":(d.state==="restarting"?"var(--warn)":(d.state==="refused"?"var(--crit)":""));$("liveserver").textContent=d.server?"Server: "+d.server.name:"";const pend=d.active&&d.pending&&d.pending.length?d.pending:[];if(!pend.length)pendIgnored="";$("livepending").hidden=!pend.length||pend.join("|")===pendIgnored;if(pend.length)$("livepending_t").textContent=`Noch nicht übernommen: ${pend.join(", ")}. `+"Das gilt erst nach einem Neustart der Sendung.";{const a=d.autostart||{},cb=$("auto_on");if(document.activeElement!==cb)cb.checked=!!a.enabled;const st=$("autostatus");if(a.phase==="wartet"){st.hidden=false;st.textContent=a.until?`Automatischer Start: wartet auf eine sendende Kamera (bis ${new Date(a.until*1000).toLocaleTimeString(LOC(),{hour:"2-digit",minute:"2-digit"})} Uhr)`:"Automatischer Start: wartet auf eine sendende Kamera";}
else if(a.phase==="aufgegeben"||a.phase==="abgebrochen"){st.hidden=false;st.textContent=a.message||"Automatischer Start beendet";}
else st.hidden=true;}
const why=[];if(d.state==="refused"&&d.message)why.push(d.message);{const nc=d.net_conflict,cards=nc&&Array.isArray(nc.cards)?nc.cards:[],nets=nc&&Array.isArray(nc.nets)?nc.nets:[];$("nethint").hidden=cards.length<2;$("nethint_cards").textContent=cards.join(", ")+(nets.length?" ("+nets.join(", ")+")":"");}
$("srvhint").hidden=!(!d.active&&d.no_server);$("livehint").hidden=!(!d.active&&d.no_layout);if(!d.active)why.push(...d.reasons.filter(r=>!(d.no_layout&&r==="Kein Bildaufbau gespeichert")&&!(d.no_server&&r==="Kein SRTLA-Server ausgewählt")));if(d.active&&d.failover&&d.failover.waiting)why.push((d.failover.inactive||[]).length?"Keine aktive Kamera sendet, warte auf ein Signal":"Keine Kamera sendet, warte auf ein Signal");else if(d.active&&d.failover&&d.failover.degraded){const nm=k=>(camsByKey[k]||{}).name||k;why.push(`Notbetrieb: es senden ${d.failover.layout.map(nm).join(", ")} (fehlende Kameras kommen nach 60 s stabilem Signal zurück)`);}
if(d.active&&d.always_note)why.push(d.always_note);if(d.active&&d.last&&d.last_age!=null&&d.last_age<180)why.push(d.last);if(d.active&&d.restarts){const e=d.restarts.belacoder||0,n=d.restarts.srtla_send||0;if(e||n)why.push("Seit dem Start automatisch neu gestartet: "+[e?`Encoder ${e}×`:"",n?`Netzverbindung ${n}×`:""].filter(Boolean).join(", "));}
$("livewhy").textContent=why.join(" · ");}
async function askRestart(){await sendLoad();const pend=sendData&&sendData.active&&sendData.pending||[];if(!pend.length||$("rdlg").open)return;$("rdlg_t").textContent="Geändert: "+pend.join(", ")+". Das gilt erst nach einem Neustart der Sendung.";$("rdlg").showModal();}
$("live_topipe").addEventListener("click",()=>{const c=$("pipecard");c.open=true;c.scrollIntoView({behavior:"smooth",block:"start"});});$("live_tosrv").addEventListener("click",()=>{const c=$("srtlacard");c.open=true;c.scrollIntoView({behavior:"smooth",block:"start"});});$("live_ignore").addEventListener("click",()=>{pendIgnored=(sendData&&sendData.pending||[]).join("|");$("livepending").hidden=true;});$("rdlg_go").addEventListener("click",async()=>{$("rdlg").close();await restartNow();});async function restartNow(){try{await srtlaCall("POST","/api/send",{action:"restart",confirm:true});$("liveerr").textContent="";}catch(x){$("liveerr").textContent=x.message;}
sendLoad();}
$("auto_help_btn").addEventListener("click",()=>{const h=$("auto_help");h.hidden=!h.hidden;$("auto_help_btn").setAttribute("aria-expanded",String(!h.hidden));});$("auto_on").addEventListener("change",async e=>{try{await srtlaCall("POST","/api/autostart",{enabled:e.target.checked});$("liveerr").textContent="";}catch(x){$("liveerr").textContent=x.message;e.target.checked=!e.target.checked;}
sendLoad();});$("live_restart").addEventListener("click",async()=>{if(!confirm("Die Sendung jetzt kurz neu starten?\n\nDas Bild ist dabei einige Sekunden weg."))return;try{await srtlaCall("POST","/api/send",{action:"restart",confirm:true});$("liveerr").textContent="";}catch(x){$("liveerr").textContent=x.message;}
sendLoad();});const isPhone=()=>window.matchMedia("(max-width:620px)").matches;async function doLiveStart(){const s=sendData&&sendData.server;if(!s)return;try{await srtlaCall("POST","/api/send",{action:"start",confirm:true});$("liveerr").textContent="";}catch(x){$("liveerr").textContent=x.message;if(isPhone())footMsg(x.message);else alert(x.message);}
setTimeout(sendLoad,800);}
function seamlessTo(main,key){const g=(sendData&&sendData.swap_group)||[];return!!(sendData&&sendData.active&&g.includes(main)&&g.includes(key));}
$("swapbtns").addEventListener("click",async e=>{const b=e.target.closest(".swapto");if(!b)return;try{const r=await srtlaCall("POST","/api/pipeline/swap",{with:b.dataset.key});$("liveerr").textContent=(r&&r.note)||"Getauscht.";pipeDirty=false;pipeLoad();}catch(x){$("liveerr").textContent=x.message;}});async function doLiveStop(){try{await srtlaCall("POST","/api/send",{action:"stop"});}catch(x){$("liveerr").textContent=x.message;}
setTimeout(sendLoad,800);}
function placeSig(){const sig=$("net_sig"),dots=$("cam_dots"),hold=$("hdr_sig"),gear=$("chat_opt"),up=$("liveup");if(!sig||!dots||!hold||!gear||!up)return;const want=isPhone()&&document.body.classList.contains("streaming");if(want){if(sig.parentNode!==hold){hold.appendChild(sig);hold.appendChild(dots);}}
else{if(sig.parentNode!==up.parentNode)up.before(sig);if(dots.parentNode!==gear.parentNode)gear.before(dots);}}
setTimeout(placeSig,0);if(window.matchMedia)matchMedia("(max-width:620px)").addEventListener("change",placeSig);$("hdr_more").addEventListener("click",()=>{const o=document.body.classList.toggle("hdr-open");$("hdr_more").setAttribute("aria-expanded",o?"true":"false");});function updHdrLive(d){{const on=!!d.active&&d.state!=="stopping";if(on!==document.body.classList.contains("streaming")){document.body.classList.toggle("streaming",on);document.body.classList.remove("hdr-open");$("hdr_more").setAttribute("aria-expanded","false");placeSig();}}
const b=$("hdr_live"),live=!!d.active,busy=d.state==="restarting"||d.state==="starting"||d.state==="stopping";b.hidden=false;b.className="livebtn"+(live&&!busy?" on":"")+(busy?" busy":"");b.textContent=busy?(LIVE_LABEL[d.state]==="Wird beendet…"?"Wird beendet…":"Verbinde …"):(live?"● LIVE":(window.matchMedia&&matchMedia("(max-width:620px)").matches?"Live":"Live gehen"));b.disabled=busy||(!live&&!d.can_start);b.title=busy?(LIVE_LABEL[d.state]||""):(live?"Es wird gesendet. Klicken beendet die Sendung.":(d.can_start?"Nicht live. Klicken startet die Sendung.":"Kann nicht starten: "+((d.reasons||[]).join(" · ")||"Bedingungen nicht erfüllt")));const pl=$("prev_live");pl.hidden=live||busy;pl.disabled=!d.can_start;pl.title=b.title;const f=$("mf_live");f.className="mf-live"+(live&&!busy?" on":"")+(busy?" busy":"");f.textContent=busy?"…":(live?"Stop":"Live");f.disabled=b.disabled;f.title=b.title;}
async function liveToggle(){if(sendData&&sendData.active)await doLiveStop();else await doLiveStart();}
$("hdr_live").addEventListener("click",liveToggle);$("prev_live").addEventListener("click",liveToggle);$("mf_live").addEventListener("click",liveToggle);const FIC=(()=>{const o='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">',x='<path d="M4 4l16 16"/></svg>';const cam='<path d="M3 8.5A2.5 2.5 0 0 1 5.5 6H8l1.5-2h5L16 6h2.5A2.5 2.5 0 0 1 21 8.5v9a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/><circle cx="12" cy="13" r="3.6"/>',pip='<rect x="3" y="5" width="18" height="14" rx="2.5"/><rect x="12" y="11" width="6" height="5" rx="1.2"/>',spk='<path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M15.5 9a4.5 4.5 0 0 1 0 6M18 6.5a8 8 0 0 1 0 11"/>',mic='<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11.5a6.5 6.5 0 0 0 13 0M12 18v3"/>';return{cam:o+cam+"</svg>",pip:o+pip+"</svg>",pipoff:o+pip+x,spk:o+spk+"</svg>",micoff:o+mic+x};})();function camLabels(cams){const w=cams.map(c=>String(c.name||c.key||"?").trim().split(/\s+/)),n=cams.map(()=>1),lab=i=>w[i].slice(0,n[i]).join(" ");for(let r=0;r<6;r++){const grow=cams.map((_,i)=>n[i]<w[i].length&&cams.some((_,j)=>j!==i&&lab(j)===lab(i)));if(!grow.some(Boolean))break;grow.forEach((g,i)=>{if(g)n[i]++;});}
return Object.fromEntries(cams.map((c,i)=>[c.key,lab(i)]));}
let footBusy=false,footMsgT=null;function footMsg(t,ms){const m=$("mf_msg");clearTimeout(footMsgT);m.textContent=t||"";m.hidden=!t;m.dataset.info="";m.style.fontSize="";if(t)footMsgT=setTimeout(()=>{m.hidden=true;},ms||6000);}
$("mf_info").addEventListener("click",()=>{const m=$("mf_msg");if(!m.hidden&&m.dataset.info==="1"){footMsg("");return;}
const lines=$("mf_tools").dataset.cams==="1"?["Kurzer Klick = zum Hauptbild machen","Doppelklick = aus-/einblenden, Ton bleibt","Langer Klick = deaktivieren / aktivieren","Ton: kurzer Klick = nächste Spur","Ton: langer Klick = stumm / wieder laut"]:["Ton: langer Klick = stumm / wieder laut"];footMsg(lines.join("\n"),15000);m.dataset.info="1";fitMsg(m);});function fitMsg(m){m.style.fontSize="";let fs=parseFloat(getComputedStyle(m).fontSize)||14;while(m.scrollWidth>m.clientWidth+1&&fs>9){fs-=.5;m.style.fontSize=fs+"px";}}
(function(){const root=document.documentElement,LK="pb_foot_layout",IK="pb_foot_info",LAY=["one","two","left","right"];let lay="one",info=true;try{const v=localStorage.getItem(LK);if(LAY.includes(v))lay=v;info=localStorage.getItem(IK)!=="0";}catch(e){}
function apply(){root.classList.toggle("mf2",lay==="two");root.classList.toggle("mfleft",lay==="left");root.classList.toggle("mfright",lay==="right");root.classList.toggle("nofootinfo",!info);if(typeof sizeFoot==="function")sizeFoot();}
window.pbFootLayout=v=>{lay=LAY.includes(v)?v:"one";try{if(lay==="one")localStorage.removeItem(LK);else localStorage.setItem(LK,lay);}catch(e){}apply();};window.pbFootLayoutGet=()=>lay;
window.pbFootInfo=b=>{info=!!b;try{if(info)localStorage.removeItem(IK);else localStorage.setItem(IK,"0");}catch(e){}apply();};window.pbFootInfoGet=()=>info;apply();})();
(function(){const f=$("mfoot"),root=document.documentElement,KEY="pb_foot";let y=Math.max(0,window.scrollY),run=0,scrollHid=false,mode="auto";try{const v=localStorage.getItem(KEY);if(v==="always"||v==="hide"||v==="off")mode=v;}catch(e){}
const shown=()=>["c_chat","c_prev","c_deck"].some(id=>{const e=$(id);return!!e&&e.open&&e.offsetParent!==null;});const apply=()=>{const wide=shown(),h=mode==="off"||(mode!=="always"&&wide&&(mode==="hide"||scrollHid));root.classList.toggle("mfhide",h);root.classList.toggle("mfperm",mode==="off"||(mode==="hide"&&wide));};window.pbFootMode=m=>{mode=(m==="always"||m==="hide"||m==="off")?m:"auto";try{localStorage.setItem(KEY,mode);}catch(e){}scrollHid=false;apply();};window.pbFootGet=()=>mode;window.addEventListener("scroll",()=&g
10BC8
t;{if(getComputedStyle(f).display==="none"){run=0;scrollHid=false;apply();return;}
const ny=Math.max(0,window.scrollY),d=ny-y;y=ny;run=(d>0)===(run>0)?run+d:d;const bottom=window.innerHeight+ny>=document.documentElement.scrollHeight-8;if(ny<24||bottom||run<-8)scrollHid=false;else if(run>8)scrollHid=true;apply();},{passive:true});f.addEventListener("focusin",()=>{scrollHid=false;apply();});["c_chat","c_prev","c_deck"].forEach(id=>{const e=$(id);if(e)e.addEventListener("toggle",apply);});window.addEventListener("resize",apply);document.addEventListener("pb-menus-changed",apply);new MutationObserver(apply).observe(document.querySelector("main"),{attributes:true,subtree:true,attributeFilter:["data-pbhide"]});apply();})();function sizeFoot(){const h=$("mfoot").offsetHeight;if(h)document.documentElement.style.setProperty("--mfh",h+"px");else document.documentElement.style.removeProperty("--mfh");}
window.addEventListener("resize",sizeFoot);if(window.ResizeObserver)new ResizeObserver(sizeFoot).observe($("mfoot"));function renderFoot(d){if(footBusy)return;const f=d.footer,box=$("mf_tools");if(!f||!f.cams||!f.cams.length){box.hidden=true;$("mf_info").hidden=true;setHtml(box,"");sizeFoot();return;}
const L=camLabels(f.cams),multi=f.cams.length>=2;const cams=!multi?"":f.cams.map(c=>{const full=c.name||c.key;const tip=(c.main?[`${full} ist das Hauptbild (lässt sich nicht ausblenden oder deaktivieren).`,"Kurz auf eine andere Kamera drücken macht diese zum Hauptbild."]:[c.inactive?`${full} ist deaktiviert.`:(c.hidden?`${full} ist ausgeblendet.`:`${full} ist als kleines Bild zu sehen.`),c.inactive?"Kurz drücken aktiviert die Kamera, das Bild bleibt ausgeblendet (Ton nutzbar).":"Kurz drücken macht es zum Hauptbild.",c.inactive?"Doppelt tippen: aktivieren, das Bild bleibt ausgeblendet.":(c.hidden?"Doppelt tippen: das kleine Bild einblenden.":"Doppelt tippen: das kleine Bild ausblenden (Ton noch da)."),c.inactive?"Lang drücken: aktivieren.":"Lang drücken: deaktivieren (nicht im Stream, Ton nicht nutzbar)."]).join(" ");return`<button type="button" class="mf-t mf-cam${c.main?" ismain":""}${c.hidden?" hid":""}${c.inactive?" off":""}${c.state==="live"?" live":""}" data-kind="cam" data-key="${esc(c.key)}" data-label="${esc(L[c.key])}" data-slot="${c.slot}" data-main="${c.main?1:0}" data-hidden="${c.hidden?1:0}" data-inactive="${c.inactive?1:0}" data-gone="${c.state==="offline"?1:0}" title="${esc(tip)}" aria-label="${esc(tip)}">${c.main?FIC.cam:(c.hidden?FIC.pipoff:FIC.pip)}<span>${esc(L[c.key])}</span></button>`;}).join("");const a=f.audio||{},an=a.name||a.key||"", atip=(a.mute?`Ton von ${an} (stumm).`:`Ton von ${an}.`)+" "+(one?(a.mute?"Lang drücken schaltet wieder laut.":"Lang drücken schaltet stumm."):(a.mute?"Kurz drücken wechselt die Tonspur, lang drücken schaltet wieder laut.":"Kurz drücken wechselt die Tonspur, lang drücken schaltet stumm."));const aud=`<button type="button" class="mf-t mf-aud${a.mute?" muted":""}" data-kind="aud" data-single="${one?1:0}" data-mute="${a.mute?1:0}" data-next="${esc(a.next||"main")}" title="${esc(atip)}" aria-label="${esc(atip)}">${a.mute?FIC.micoff:FIC.spk}<span>Ton: ${esc(L[a.key]||an)}</span></button>`;box.hidden=false;box.dataset.cams=multi?"1":"0";$("mf_info").hidden=false;setHtml(box,cams+aud);sizeFoot();}
async function footAct(b,long,dbl){const d=sendData||{},f=d.footer||{cams:[]},mainKey=(f.cams.find(c=>c.main)||{}).key;try{let r;if(b.dataset.kind==="cam"){const isMain=b.dataset.main==="1",on=b.dataset.inactive==="1";if(long){if(isMain)return footMsg("Das Hauptbild lässt sich nicht deaktivieren.");r=await srtlaCall("POST","/api/pipeline/active",{key:b.dataset.key,active:on});return footMsg(on?"Aktiviert.":"Deaktiviert: nicht mehr im Stream, Ton nicht nutzbar.");}
if(isMain)return footMsg(dbl?"Das Hauptbild lässt sich nicht ausblenden. Zum Tauschen kurz auf eine andere Kamera drücken.":"Das ist schon das Hauptbild.");if(!dbl&&!on&&b.dataset.gone==="1")return footMsg("Diese Kamera ist nicht verbunden und lässt sich nicht zum Hauptbild machen.");if(on){await srtlaCall("POST","/api/pipeline/active",{key:b.dataset.key,active:true});r=await srtlaCall("POST","/api/pipeline/view",{visible:{[b.dataset.slot]:false}});return footMsg("Aktiviert, Bild ausgeblendet: Ton nutzbar.");}
if(dbl)r=await srtlaCall("POST","/api/pipeline/view",{visible:{[b.dataset.slot]:b.dataset.hidden==="1"}});else r=await srtlaCall("POST","/api/pipeline/swap",{with:b.dataset.key});}else if(long){if(!d.active)return footMsg("Stumm schalten geht nur während der Sendung.");r=await srtlaCall("POST","/api/pipeline/view",{mute:b.dataset.mute!=="1"});}else{if(b.dataset.single==="1")return footMsg("Es gibt nur eine Tonquelle. Lang drücken schaltet stumm oder wieder laut.");r=await srtlaCall("POST","/api/pipeline/view",{audio:b.dataset.next});}
footMsg((r&&r.note)||"");}catch(x){footMsg(x.message);}
finally{footBusy=false;sendLoad();}}
(function(){const box=$("mf_tools");let timer=null,fired=false,x0=0,y0=0;const stop=()=>{if(timer){clearTimeout(timer);timer=null;}};box.addEventListener("pointerdown",e=>{const b=e.target.closest(".mf-t");if(!b||(e.pointerType==="mouse"&&e.button!==0))return;footBusy=true;fired=false;x0=e.clientX;y0=e.clientY;stop();timer=setTimeout(()=>{timer=null;fired=true;try{if(navigator.vibrate)navigator.vibrate(25);}catch(x){}footAct(b,true);},550);});box.addEventListener("pointermove",e=>{if(timer&&Math.hypot(e.clientX-x0,e.clientY-y0)>12){stop();footBusy=false;}});["pointerup","pointercancel"].forEach(n=>box.addEventListener(n,()=>{stop();if(!fired)setTimeout(()=>{footBusy=false;},400);}));let tapT=null,tapKey="";box.addEventListener("click",e=>{const b=e.target.closest(".mf-t");if(!b)return;if(fired){fired=false;e.preventDefault();return;}
if(b.dataset.kind!=="cam"){footAct(b,false);return;}
if(tapT&&tapKey===b.dataset.key){clearTimeout(tapT);tapT=null;footAct(b,false,true);return;}
clearTimeout(tapT);tapKey=b.dataset.key;footBusy=true;tapT=setTimeout(()=>{tapT=null;footAct(b,false);},300);});box.addEventListener("contextmenu",e=>e.preventDefault());})();sendLoad();setInterval(()=>{if(!document.hidden)sendLoad();},2000);let rBusy=false,rDeclinedMem=false;const R_STATE={Running:"verbunden",NeedsLogin:"Anmeldung nötig",Stopped:"getrennt",NoState:"startet …",Starting:"startet …"};async function rLoad(){let d;try{d=await srtlaCall("GET","/api/remote");}catch(e){return;}
rBusy=d.state==="working";$("r_badge").textContent=!d.installed?"nicht installiert":(rBusy?"läuft …":(R_STATE[d.backend]||d.backend||"–")+(d.connected&&d.serve?", freigegeben":""));$("r_badge").className="badge "+(d.connected&&d.serve?"ok":"attn");const info=$("r_info");info.innerHTML="";if(d.installed&&d.connected){for(const[k,v]of[["Gerätename",d.name],["Adresse im Tailscale-Netz",d.ip],["Tailscale-Netz",d.tailnet]]){if(!v)continue;const r=document.createElement("div");r.className="row";const a=document.createElement("span");a.textContent=k;const b=document.createElement("b");b.textContent=v;r.append(a,b);info.append(r);}}
const login=d.login_url&&/^https:\/\/(login|console)\.tailscale\.com\//.test(d.login_url);$("r_login").hidden=!login;if(login)$("r_loginlink").href=d.login_url;$("r_https").hidden=!(d.installed&&d.connected&&!d.serve);{const hu=d.hint_url&&/^https:\/\/(login|console)\.tailscale\.com\//.test(d.hint_url);$("r_httpslink").href=hu?d.hint_url:"https://login.tailscale.com/admin/dns";}
const ur=$("r_url_row");ur.hidden=!(d.connected&&d.serve&&d.url);if(!ur.hidden){$("r_url").textContent=d.url;$("r_url").href=d.url;}
{const w=$("r_warn");w.hidden=!d.funnel;if(d.funnel)w.textContent="Achtung: Die Oberfläche ist öffentlich im Internet erreichbar (Funnel). Das bleibt so, auch nach einem Neustart der Box, bis Sie die Freigabe mit „Öffentliche Freigabe beenden“ abschalten.";}
const acts=$("r_actions");acts.innerHTML="";const FUNNEL_ASK="Die Oberfläche öffentlich im Internet freigeben (Funnel)?\n\nDanach kann JEDER im Internet, der die Adresse der Box kennt, die Anmeldeseite öffnen, auch ohne Tailscale. Geschützt ist die Box dann nur durch das BELABOX-Passwort.\n\n• Nur mit einem starken, langen Passwort verwenden.\n• Die Freigabe bleibt an, auch nach einem Neustart der Box, bis Sie sie mit „Öffentliche Freigabe beenden“ abschalten.\n• Der Gerätename steht in öffentlichen Zertifikatslisten und wird von Suchprogrammen gefunden, die Anmeldeseite wird dann ausprobiert.\n\nWenn Sie nur von eigenen Geräten zugreifen wollen, reicht die Freigabe im privaten Netz (Tailscale-App auf dem Gerät).\n\nWirklich öffentlich freigeben?";const add=(txt,act,cls,ask,extra)=>{const b=document.createElement("button");b.type="button";if(cls)b.className=cls;b.textContent=txt;b.disabled=rBusy||!d.helper_installed;b.addEventListener("click",async()=>{if(ask&&!confirm(ask))return;try{await srtlaCall("POST","/api/remote",Object.assign({action:act,confirm:true},extra||{}));$("r_msg").textContent="Wird ausgeführt …";}catch(x){$("r_msg").textContent=x.message;}setTimeout(rLoad,1200);});acts.append(b);};if(!d.installed){let declined=rDeclinedMem;try{declined=declined||localStorage.getItem("rDeclined")==="1";}catch(e){}
const choice=(txt,cls,fn)=>{const b=document.createElement("button");b.type="button";if(cls)b.className=cls;b.textContent=txt;b.disabled=rBusy||!d.helper_installed;b.addEventListener("click",fn);acts.append(b);};if(declined){$("r_badge").textContent="nicht eingerichtet";$("r_badge").className="badge";choice("Doch einrichten","sec",()=>{rDeclinedMem=false;try{localStorage.removeItem("rDeclined");}catch(e){}rLoad();});}else{info.innerHTML='<div class="ph" style="margin-top:0"><b>Möchten Sie den Fernzugriff einrichten?</b> Das ist freiwillig. Ohne Fernzugriff funktioniert alles wie bisher im lokalen Netz. Es wird nur etwas installiert, wenn Sie „Ja“ wählen.</div>';choice("Ja, einrichten","",async()=>{if(!confirm("Tailscale aus der offiziellen Paketquelle installieren?\n\nDas lädt ein kleines Programm herunter. Es ändert nichts am BELABOX-System."))return;try{await srtlaCall("POST","/api/remote",{action:"install",confirm:true});$("r_msg").textContent="Wird ausgeführt …";}catch(x){$("r_msg").textContent=x.message;}setTimeout(rLoad,1200);});choice("Nein, nicht einrichten","sec",()=>{rDeclinedMem=true;try{localStorage.setItem("rDeclined","1");}catch(e){}$("rcard").open=false;rLoad();});}}
else if(!d.connected)add(login?"Neuen Anmeldelink holen":"Verbinden","login","");else{if(!d.serve)add("Oberfläche im privaten Netz freigeben","serve_on","");else add("Freigabe beenden","serve_off","sec");if(d.funnel)add("Öffentliche Freigabe beenden","funnel_off","");else add("Öffentlich im Internet freigeben (Funnel) …","funnel_on","sec",FUNNEL_ASK,{public:true});add("Verbindung trennen","down","sec","Verbindung zu Tailscale trennen? Der Fernzugriff geht dann nicht mehr.");add("Vom Konto abmelden","logout","sec","Diese Box aus Ihrem Tailscale-Konto abmelden? Danach müssen Sie sich neu anmelden.");}
let msg=d.message||"";if(!d.helper_installed)msg="Der Fernzugriff-Helfer ist nicht installiert. Bitte install.sh einmal erneut ausführen.";else if(rBusy)msg=(d.step||"Arbeite")+" …";if(d.hint_url&&/^https:\/\/(login|console)\.tailscale\.com\//.test(d.hint_url)&&$("r_https").hidden){msg+=" ";$("r_msg").textContent=msg;const a=document.createElement("a");a.href=d.hint_url;a.target="_blank";a.rel="noopener noreferrer";a.textContent="Tailscale-Seite öffnen";$("r_msg").append(a);}
else $("r_msg").textContent=msg;$("r_msg").style.color=d.state==="failed"?"var(--crit)":"";ssLoad();}
function ssRender(d){$("ss_nc").hidden=!(d.installed&&!d.connected);const on=$("ss_on");if(on!==document.activeElement)on.checked=!!d.enabled;on.disabled=!d.installed||!d.connected||d.busy;$("ss_body").hidden=!d.enabled;document.querySelectorAll("[data-ssg]").forEach(c=>{if(c!==document.activeElement)c.checked=(d.groups||{})[c.dataset.ssg]!==false;});$("ss_page").value=d.page_url||"";$("ss_ws").value=d.ws_url||"";$("ss_cpage").disabled=!d.page_url;$("ss_cws").disabled=!d.ws_url;$("ss_dl").disabled=!d.ws_url;$("ss_clients").textContent=d.public?`Betrachter jetzt: ${d.clients} von 3`:"";const m=$("ss_msg");m.textContent="";if(d.busy)m.textContent="Wird ausgeführt …";else if(d.enabled&&!d.public)m.textContent=d.message||"Die Freigabe ist noch nicht aktiv.";else if(d.state==="needs_funnel"&&d.message)m.textContent=d.message;if(d.hint_url&&/^https:\/\/(login|console)\.tailscale\.com\//.test(d.hint_url)&&(d.state==="needs_funnel")){const a=document.createElement("a");a.href=d.hint_url;a.target="_blank";a.rel="noopener noreferrer";a.textContent=" Tailscale-Seite öffnen";m.append(a);}}
async function ssLoad(){try{const r=await fetch("/api/statsshare");if(r.ok)ssRender(await r.json());}catch(e){}}
async function ssPost(body){$("ss_err").textContent="";try{ssRender(await srtlaCall("POST","/api/statsshare",body));setTimeout(rLoad,1500);}catch(x){$("ss_err").textContent=x.message;ssLoad();}}
$("ss_on").addEventListener("change",e=>{if(e.target.checked){if(!confirm("Die Statistik öffentlich im Internet freigeben?\n\nDanach kann JEDER, der die Adresse mit dem Schlüssel kennt, die freigegebenen Werte lesen (höchstens 3 gleichzeitig), auch ohne Tailscale. Die Oberfläche der Box bleibt davon unberührt.\n\nDie Freigabe bleibt auch nach einem Neustart der Box, bis Sie sie wieder ausschalten.")){e.target.checked=false;return;}
ssPost({enabled:true});}else ssPost({enabled:false});});document.querySelectorAll("[data-ssg]").forEach(c=>c.addEventListener("change",()=>ssPost({groups:{[c.dataset.ssg]:c.checked}})));$("ss_new").addEventListener("click",()=>{if(confirm("Neuen Schlüssel erzeugen? Die bisherige Adresse und schon heruntergeladene Dateien funktionieren danach nicht mehr."))ssPost({new_token:true});});for(const[btn,field]of[["ss_cpage","ss_page"],["ss_cws","ss_ws"]])$(btn).addEventListener("click",async()=>{const b=$(btn),old=b.textContent,ok=await copyText($(field).value);b.textContent=ok?"Kopiert":old;clearTimeout(b._t);b._t=setTimeout(()=>{b.textContent=old;},1800);});$("ss_dl").addEventListener("click",async()=>{$("ss_err").textContent="";try{const r=await fetch("/api/statsshare/widget");if(!r.ok){const j=await r.json().catch(()=>({}));throw new Error(j.error||"Fehler");}
const a=document.createElement("a");a.href=URL.createObjectURL(await r.blob());a.download="statistik.html";document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),5000);}catch(x){$("ss_err").textContent=x.message;}});function copyFallback(t){const ta=document.createElement("textarea");ta.value=t;ta.setAttribute("readonly","");ta.style.cssText="position:fixed;left:-9999px;top:0";document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,t.length);let ok=false;try{ok=document.execCommand("copy");}catch(e){}
ta.remove();return ok;}
async function copyText(t){if(navigator.clipboard&&window.isSecureContext){try{await navigator.clipboard.writeText(t);return true;}catch(e){}}
return copyFallback(t);}
document.addEventListener("click",async e=>{const el=e.target.closest&&e.target.closest(".pwshow");if(!el)return;const ok=await copyText(el.textContent);el.classList.remove("copied","copyfail");el.classList.add(ok?"copied":"copyfail");clearTimeout(el._t);el._t=setTimeout(()=>el.classList.remove("copied","copyfail"),2500);});$("r_copy").addEventListener("click",async()=>{const t=$("r_url").textContent;let ok=false;try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(t);ok=true;}}catch(e){}
if(!ok){const ta=document.createElement("textarea");ta.value=t;ta.setAttribute("readonly","");ta.style.cssText="position:fixed;left:-9999px;top:0";document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,t.length);try{ok=document.execCommand("copy");}catch(e){}
ta.remove();}
if(!ok){const rng=document.createRange();rng.selectNodeContents($("r_url"));const sel=window.getSelection();sel.removeAllRanges();sel.addRange(rng);}
$("r_copy").textContent=ok?"Kopiert":"Markiert: Strg/Cmd+C";setTimeout(()=>$("r_copy").textContent="Kopieren",2500);});(function rLoop(){(rBusy||due("r","rcard",60000)?rLoad():Promise.resolve()).finally(()=>setTimeout(rLoop,rBusy?2000:6000));})();let swFirst=null,swPending=0;const SW_TERMINAL=["failed","refused","rolledback"];let swSaw=false;function swBegin(text){swPending=pbUpdating=Date.now();swSaw=false;["sw_install","sw_check","sw_switch","sw_rollback"].forEach(id=>$(id).disabled=true);$("sw_badge").textContent="läuft …";$("sw_badge").className="badge attn";$("sw_msg").textContent=text;$("sw_msg").style.color="";progSet("swprog",true,0,"Update wird gestartet",swPending);}
function fmtNotes(t){const inl=x=>esc(x).replace(/\*\*(.+?)\*\*/g,"<b>$1</b>").replace(/`([^`]+)`/g,"<code>$1</code>");let out="",list=false;const close=()=>{if(list){out+="</ul>";list=false;}};String(t||"").split("\n").forEach(raw=>{const l=raw.replace(/\s+$/,"");if(/^##\s+/.test(l)){close();out+=`<h4>${inl(l.replace(/^##\s+/,""))}</h4>`;}
else if(/^\s*[-*]\s+/.test(l)){if(!list){out+="<ul>";list=true;}out+=`<li>${inl(l.replace(/^\s*[-*]\s+/,""))}</li>`;}
else if(!l.trim())close();else if(list&&/^\s+\S/.test(l))out=out.replace(/<\/li>$/," "+inl(l.trim())+"</li>");else{close();out+=`<p>${inl(l)}</p>`;}});close();return out;}
(function(){const btn=$("sw_histbtn"),panel=$("sw_hist"),q=$("sw_hq"),cnt=$("sw_hcount"),res=$("sw_hres");let secs=null,busy=false,tm=0, parse(text){const out=[];let cur=null,it=null;String(text||"").split("\n").forEach(raw=>{const l=raw.replace(/\s+$/,"");if(/^##\s+/.test(l)){cur={head:l,items:[]};out.push(cur);it=null;}
else if(!cur)return;else if(/^\s*[-*]\s+/.test(l)){it=[l];cur.items.push(it);}
else if(!l.trim())it=null;else if(it&&/^\s+\S/.test(l))it.push(l);else{it=[l];cur.items.push(it);}});return out;}
function mark(root,toks){if(!toks.length)return;const re=new RegExp("("+toks.map(w=>w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|")+")","gi");const wk=document.createTreeWalker(root,NodeFilter.SHOW_TEXT),nodes=[];while(wk.nextNode())nodes.push(wk.currentNode);for(const n of nodes){const t=n.nodeValue;re.lastIndex=0;if(!re.test(t))continue;re.lastIndex=0;const fr=document.createDocumentFragment();let last=0,m;while((m=re.exec(t))){if(!m[0].length){re.lastIndex++;continue;}if(m.index>last)fr.appendChild(document.createTextNode(t.slice(last,m.index)));const k=document.createElement("mark");k.textContent=m[0];fr.appendChild(k);last=m.index+m[0].length;}
if(last<t.length)fr.appendChild(document.createTextNode(t.slice(last)));n.parentNode.replaceChild(fr,n);}}
function render(){if(!secs)return;const toks=q.value.toLowerCase().split(/\s+/).filter(Boolean).slice(0,6);let out="",hits=0,vers=0;const vre=only?new RegExp("^##\\s+v?"+only.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(\\s|$|\\()"):null;for(const sec of secs){if(vre&&!vre.test(sec.head))continue;const hl=sec.head.toLowerCase();const pick=toks.length?sec.items.filter(it=>{const t=hl+" "+it.join(" ").toLowerCase();return toks.every(w=>t.includes(w));}):sec.items;if(toks.length&&!pick.length)continue;vers++;hits+=pick.length;out+=sec.head+"\n"+pick.map(it=>it.join("\n")).join("\n")+"\n\n";}
res.dataset.h="";res.innerHTML=fmtNotes(out);mark(res,toks);cnt.textContent=only?(vers?`Änderungen der Version ${only}. Ein Stichwort im Suchfeld zeigt alle Versionen.`:`Zu Version ${only} steht nichts in der Änderungsliste. Ein Stichwort im Suchfeld zeigt alle Versionen.`):toks.length?(hits?`${hits} Treffer in ${vers} Versionen`:"Keine Treffer."):`${secs.length} Versionen. Zum Suchen ein Stichwort eintippen.`;}
async function load(){if(busy)return;busy=true;cnt.textContent="Wird geladen …";try{const r=await fetch("/api/swupdate/history");const d=r.ok?await r.json():null;if(!d||d.error||!d.text){cnt.textContent=(d&&d.error)||"Der Verlauf konnte nicht geladen werden.";}
else{secs=parse(d.text);render();}}
catch(e){cnt.textContent="Der Verlauf konnte nicht geladen werden.";}
busy=false;}
btn.addEventListener("click",()=>{const open=panel.hidden;panel.hidden=!open;btn.setAttribute("aria-expanded",String(open));if(open){ load();q.focus();}});q.addEventListener("input",()=>{ v=window.swCurVer;if(!v)return; await load();panel.scrollIntoView({block:"nearest",behavior:"smooth"});});})();function swFinish(cur){pbUpdating=swPending=0;if(cur){try{sessionStorage.setItem("pb_updated",cur);}catch(e){}}
progSet("swprog",true,100,"");$("sw_msg").textContent="Das Update ist fertig. Die Seite lädt neu und zeigt bei Bedarf die Anmeldung.";$("sw_msg").style.color="";$("sw_badge").textContent="fertig";$("sw_badge").classList.remove("attn");$("sw_badge").classList.add("ok");setTimeout(()=>location.reload(),2000);}
async function swPublic(){let p;try{const r=await fetch("/api/swprogress");p=await r.json();}catch(e){$("sw_msg").textContent="Die Oberfläche startet gerade neu, einen Moment …";return;}
if(p.state==="installing"){swSaw=true;progSet("swprog",true,p.progress,p.step);$("sw_msg").textContent="Die Oberfläche startet gerade neu, das Update läuft weiter. Die Seite lädt danach von selbst neu.";return;}
if(p.state==="done"||p.state==="rolledback"){swFinish();return;}
if(p.state==="failed"||p.state==="refused"){pbUpdating=swPending=0;progSet("swprog",false);$("sw_msg").textContent="Das Update ist fehlgeschlagen. Die Seite lädt neu; nach der Anmeldung steht die Meldung in dieser Karte.";$("sw_msg").style.color="var(--crit)";setTimeout(()=>location.reload(),4000);return;}
if(Date.now()-pbUpdating>120000)location.reload();}
async function swLoad(force,fresh){let d,r;try{r=await fetch("/api/swupdate"+(force?"?check=1":(fresh?"?fresh=1":"")));}
catch(e){if(pbUpdating)swPublic();return;}
if(r.status===401){if(pbUpdating)swPublic();return;}
d=await r.json().catch(()=>null);if(!r.ok||!d){if(pbUpdating)$("sw_msg").textContent="Die Oberfläche startet gerade neu, einen Moment …";return;}
if(swFirst===null)swFirst=d.current;const installing=d.state==="installing";if(installing){swSaw=true;if(!pbUpdating)pbUpdating=Date.now();}
progSet("swprog",installing||!!swPending&&!["failed","refused"].includes(d.state)&&!(d.state==="done"||d.state==="rolledback"),d.progress||0,d.step||"Update wird gestartet",pbUpdating);if(pbUpdating&&!swPending&&!installing){pbUpdating=0;progSet("swprog",false);}
if(swFirst!==d.current&&!installing){swFinish(d.current);return;}
if(swPending){const age=Date.now()-swPending;if((d.state==="done"||(d.state==="rolledback"&&swFirst!==d.current))&&(swSaw||age>25000)){swFinish(d.current);return;}
if(age>10000&&SW_TERMINAL.includes(d.state)){swPending=pbUpdating=0;progSet("swprog",false);}
else if(age>900000){swPending=pbUpdating=0;progSet("swprog",false);}}
{const dot=$("hdr_upd");dot.hidden=!d.newer;if(d.newer){const t=(d.latest?`Neue Version v${d.latest} verfügbar (jetzt v${d.current}).`:`Neue Version verfügbar (jetzt v${d.current}).`)+" "+(d.sending?"Einspielen geht nicht während der Übertragung.":"Zum Software-Update.");dot.title=t;dot.setAttribute("aria-label",t);}}
const busy=d.state==="installing"||!!swPending;$("stagefoot").textContent=d.stage?`IRL4YOU BOX Version ${d.current} (${d.stage}) – Funktionen und Bedienung können sich noch ändern.`:`IRL4YOU BOX Version ${d.current}`;$("sw_cur").textContent="v"+d.current+(d.stage?" ("+d.stage+")":"");window.swCurVer=d.current||"";$("sw_curinfo").hidden=!d.current;$("sw_latest").textContent=d.latest?"v"+d.latest:(d.error?"–":"wird geprüft …");$("sw_badge").textContent=busy?"läuft …":(d.error?"unbekannt":(d.newer?"Update verfügbar":(d.channel==="test"?"Datenschutz-Test":"aktuell")));$("sw_badge").className="badge "+(d.newer||busy||d.channel==="test"?"attn":"ok");const n=$("sw_notes");n.hidden=!(d.newer&&d.notes);if(d.newer&&d.notes)setHtml(n,fmtNotes(d.notes));else if(n.dataset.h){n.dataset.h="";n.innerHTML="";}
$("sw_install").disabled=busy||!d.newer||d.sending||!d.helper_installed;$("sw_install").className=d.newer?"warnbtn":"sec";$("sw_check").disabled=busy;$("sw_rollback").hidden=!d.backup_version||busy;if(d.backup_version)$("sw_rollback").textContent=`Zurück zu Version ${d.backup_version}`;let msg="";if(!d.helper_installed)msg="Der Update-Helfer ist nicht installiert. Bitte install.sh einmal erneut ausführen.";else if(d.error)msg=d.error;else if(busy)msg=(d.state==="installing"?"Das Update läuft":"Update wird gestartet")+" … Die Seite lädt danach von selbst neu.";else if(d.state==="done"||d.state==="rolledback")msg=d.message;else if(d.state==="refused"||d.state==="failed")msg=d.message;else if(d.sending&&d.newer)msg="Update gesperrt: Es wird gerade gesendet.";else if(d.newer)msg="Eine neue Version ist da. Installieren geht nur, wenn nicht gesendet wird.";else if(d.latest)msg="Sie haben die neueste Version."+(d.checked_at?` Zuletzt geprüft um ${new Date(d.checked_at*1000).toLocaleTimeString(LOC())}.`:"");$("sw_msg").textContent=msg;$("sw_msg").style.color=(d.state==="failed"||d.state==="refused"||d.error)&&!swPending?"var(--crit)":"";swVersions=d.versions||[];swCurrent=d.current;const dl=$("sw_vers");const sig=JSON.stringify(swVersions.map(v=>[v.version,v.local,v.github]));if(dl.dataset.sig!==sig){dl.dataset.sig=sig;dl.innerHTML=swVersions.filter(v=>!v.current).map(v=>`<option value="${esc(v.version)}">${esc(v.version)} · ${v.relation}${v.local?" · gesichert":""}${v.github?" · GitHub":""}${v.date?" · "+esc(v.date):""}</option>`).join("");}
$("sw_switch").disabled=busy||d.sending||!d.helper_installed;swShowInfo();swTestUi(d,busy);}
function swTestUi(d,busy){const test=d.channel==="test",tl=d.test_latest||"",sl=d.stable_latest||"";$("sw_test").hidden=!test&&!tl;$("sw_test_warn").style.fontWeight=test?"600":"";$("sw_test_oklbl").hidden=test;$("sw_test_on").hidden=test;$("sw_test_off").hidden=!test;$("sw_test_on").disabled=busy||d.sending||!d.helper_installed||!tl||!$("sw_test_ok").checked;$("sw_test_off").disabled=busy||d.sending||!d.helper_installed;$("sw_test_off").textContent=sl?`Zurück auf die stabile Version ${sl}`:"Zurück auf die stabile Version";$("sw_test_info").textContent=test?`Sie nutzen den Datenschutz-Test (Version ${d.current}).`+(d.newer?` Eine neuere Testversion ${d.latest} liegt bereit; sie lässt sich oben mit „Update installieren“ einspielen.`:""):(tl?`Aktuelle Testversion: ${tl}. Beim Wechsel wird die jetzige Version gesichert.`:"Zurzeit gibt es keine Testversion.");if(test&&!d.stage)$("stagefoot").textContent=`IRL4YOU BOX Version ${d.current} (Datenschutz-Test) – nicht für wichtige Sendungen.`;}
$("sw_test_ok").addEventListener("change",()=>swLoad(false));$("sw_test_on").addEventListener("click",async()=>{if(!confirm("Datenschutz-Test einschalten?\n\nACHTUNG: Das ist eine Testversion für das automatische Verpixeln, nur zum Ausprobieren und nicht für wichtige Sendungen. Sie belastet die Box stärker und heizt sie auf.\n\n• Es darf KEINE Übertragung laufen.\n• Die Oberfläche startet dabei kurz neu, die Einrichtung kann einige Minuten dauern.\n• Die jetzige Version wird vorher gesichert; zurück geht es jederzeit hier."))return;swBegin("Wechsel wird gestartet …");try{await srtlaCall("POST","/api/swupdate",{action:"test",confirm:true});}catch(x){swPending=pbUpdating=0;progSet("swprog",false);$("sw_msg").textContent=x.message;$("sw_msg").style.color="var(--crit)";}
setTimeout(()=>swLoad(false),1500);});$("sw_test_off").addEventListener("click",async()=>{if(!confirm("Zurück auf die stabile Version?\n\n• Es darf KEINE Übertragung laufen.\n• Die Oberfläche startet dabei kurz neu.\n• Die Testfunktionen werden entfernt, Kameras und Einstellungen bleiben erhalten."))return;swBegin("Wechsel wird gestartet …");try{await srtlaCall("POST","/api/swupdate",{action:"stable",confirm:true});}catch(x){swPending=pbUpdating=0;progSet("swprog",false);$("sw_msg").textContent=x.message;$("sw_msg").style.color="var(--crit)";}
setTimeout(()=>swLoad(false),1500);});
let swVersions=[],swCurrent="";function swFind(v){v=(v||"").trim().replace(/^v/,"");return swVersions.find(x=>x.version===v);}
function swShowInfo(){const raw=$("sw_ver").value.trim();const x=swFind(raw);let t="";if(!raw)t=swVersions.length>1?"Verfügbar: "+swVersions.filter(v=>!v.current).slice(0,6).map(v=>v.version+(v.relation==="älter"?" (älter)":" (neuer)")).join(", ")+".":"Es sind noch keine anderen Versionen gesichert oder veröffentlicht.";else if(!x||x.current)t=x&&x.current?"Diese Version ist schon installiert.":"Diese Version kennt die Box nicht (weder gesichert noch auf GitHub veröffentlicht).";else t=`Version ${x.version}: ${x.relation}${x.local?", lokal gesichert (sofort verfügbar)":""}${x.github?", auf GitHub veröffentlicht":""}${x.date?", "+x.date:""}.`+(x.notes?"\n"+x.notes.split("\n").slice(0,4).join("\n"):"");$("sw_verinfo").textContent=t;$("sw_verinfo").style.whiteSpace="pre-wrap";}
$("sw_ver").addEventListener("input",swShowInfo);$("sw_switch").addEventListener("click",async()=>{const x=swFind($("sw_ver").value);if(!x||x.current){swShowInfo();return;}
const older=x.relation==="älter";if(!confirm(`Auf Version ${x.version} wechseln?\n\n`+(older?`ACHTUNG: Das ist eine ÄLTERE Version als die installierte (${swCurrent}). Neuere Funktionen und Korrekturen fehlen dann.\n\n`:"")
+"• Es darf KEINE Übertragung laufen.\n• Die Oberfläche startet dabei kurz neu.\n• Die jetzige Version wird vorher gesichert, Sie können wieder zurück."))return;swBegin("Wechsel wird gestartet …");try{await srtlaCall("POST","/api/swupdate",{action:"switch",version:x.version,older,confirm:true});}catch(e){swPending=pbUpdating=0;progSet("swprog",false);$("sw_msg").textContent=e.message;$("sw_msg").style.color="var(--crit)";}
setTimeout(()=>swLoad(false),1500);});try{const hd=document.querySelector("header"),setH=()=>document.documentElement.style.setProperty("--hdrh",hd.offsetHeight+"px");setH();if(window.ResizeObserver)new ResizeObserver(setH).observe(hd);else window.addEventListener("resize",setH);}catch(e){}
$("hdr_sys").addEventListener("click",e=>{e.preventDefault();const c=$("updcard");if(c){c.open=true;c.scrollIntoView({behavior:"smooth",block:"start"});}});$("hdr_upd").addEventListener("click",e=>{e.preventDefault();const c=$("swcard");if(c){c.open=true;c.scrollIntoView({behavior:"smooth",block:"start"});}});$("sw_check").addEventListener("click",()=>{$("sw_msg").textContent="Suche …";swLoad(true);});$("sw_install").addEventListener("click",async()=>{if(!confirm("Software-Update installieren?\n\n• Es darf KEINE Übertragung laufen.\n• Die Oberfläche startet dabei kurz neu (etwa eine halbe Minute).\n• Kameras und Einstellungen bleiben erhalten; der Kamera-Dienst startet nur neu, wenn sich seine Dateien ändern.\n• Die jetzige Version wird gesichert, Sie können zurückgehen."))return;swBegin("Update wird gestartet …");try{await srtlaCall("POST","/api/swupdate",{action:"install",confirm:true});}catch(x){swPending=pbUpdating=0;progSet("swprog",false);$("sw_msg").textContent=x.message;$("sw_msg").style.color="var(--crit)";}
setTimeout(()=>swLoad(false),1500);});$("sw_rollback").addEventListener("click",async()=>{if(!confirm("Zur vorherigen Version zurückgehen?\n\nDie Oberfläche startet dabei kurz neu. Es darf nicht gesendet werden."))return;swBegin("Wird zurückgestellt …");try{await srtlaCall("POST","/api/swupdate",{action:"rollback",confirm:true});}catch(x){swPending=pbUpdating=0;progSet("swprog",false);$("sw_msg").textContent=x.message;$("sw_msg").style.color="var(--crit)";}
setTimeout(()=>swLoad(false),1500);});swLoad(false,true);$("swcard").addEventListener("toggle",()=>{if($("swcard").open)swLoad(false,true);});(function swLoop(){(swPending||pbUpdating||due("sw","swcard",60000)?swLoad(false):Promise.resolve()).finally(()=>setTimeout(swLoop,swPending||pbUpdating?2000:8000));})();try{const v=sessionStorage.getItem("pb_updated");if(v){sessionStorage.removeItem("pb_updated");const t=$("updtoast");t.textContent=`✔ Update installiert: Die Box läuft jetzt mit Version ${v}.`;t.hidden=false;setTimeout(()=>t.hidden=true,9000);}}catch(e){}
function renderDetails(m){if(!$("statdet").open)return;const d=m.details||{},drow=(l,v,t)=>`<div class="row" style="padding:2px 0"><span class="muted"${t?`title="${esc(t)}"`:""}>${l}</span><span>${v}</span></div>`;const f1=x=>x==null||x<0?"–":(+x).toFixed(1),n0=x=>x==null||x<0?"–":String(Math.round(x));const s=d.send;$("det_send").innerHTML=!s?'<span class="muted">Keine Daten (die Sendung läuft nicht).</span>':drow("Bitrate",`<b>${f1(upShown(m))}</b> Mbit/s <span class="muted">(eingestellt ${f1(s.bitrate_kbps/1000)})</span>`)
+drow("Laufzeit (Ping, RTT)",`<b>${f1(s.rtt_ms)}</b> ms`)
+Object.entries(m.uplinks||{}).filter(([n,u])=>u&&u.srtt!=null).map(([n,u])=>drow('<span class="muted">· '+esc(ifd(n))+'</span>',`${n0(u.srtt)} ms`)).join("")
+drow("Sendepuffer",`<b>${n0(s.snd_buf_ms)}</b> ms <span class="muted">(${n0(s.snd_buf_pkts)} Pakete)</span>`)
+drow("Paketverlust",`<b>${n0(s.loss_total)}</b> <span class="muted">(${s.loss_pct==null?"–":s.loss_pct.toFixed(2)} %)</span>`)
+drow("Encoder-Bilder pro Sekunde",`<b>${f1(s.fps)}</b>`);const cores=(m.cpu&&m.cpu.cores)||[],fr=(m.cpu&&m.cpu.freq_mhz)||[],ac=d.accel||{};const bar=p=>`<div class="bar" style="margin:1px 0 3px"><i style="width:${Math.max(0,Math.min(100,p||0))}%"></i></div>`;$("det_cpu").innerHTML=cores.map((c,i)=>drow(`Kern ${i}`,`<b>${fmt(c,0)}</b> % <span class="muted">${fr[i]?(fr[i]>=1000?(fr[i]/1000).toFixed(1)+" GHz":fr[i]+" MHz"):""}</span>`)+bar(c)).join("")
+(ac.gpu?drow("GPU",`<b>${ac.gpu.load_pct}</b> % <span class="muted">${ac.gpu.mhz} MHz</span>`)+bar(ac.gpu.load_pct):"")+(ac.npu?drow("NPU",`<b>${ac.npu.load_pct}</b> % <span class="muted">${ac.npu.mhz} MHz</span>`)+bar(ac.npu.load_pct):"");$("det_temp").innerHTML=(d.temps||[]).map(t=>drow(esc(t.name),`<b>${f1(t.c)}</b> °C`)).join("")||'<span class="muted">keine Daten</span>';const k=d.disk;$("det_disk").innerHTML=!k?'<span class="muted">keine Daten</span>':drow("Belegt",`<b>${f1(k.used_gb)}</b> GB <span class="muted">(${f1(k.used_pct)} %)</span>`)+bar(k.used_pct)
+drow("Frei",`<b>${f1(k.free_gb)}</b> GB`)+drow("Gesamt",`${f1(k.total_gb)} GB`);}
$("statdet").addEventListener("toggle",()=>{if($("statdet").open)tick();});function upTotal(m){const upl=((srtlaData&&srtlaData.settings)||{}).uplinks||[];let nets=Object.entries(m.net||{});const sel=nets.filter(([n])=>upl.includes(n));if(sel.length)nets=sel;return nets.reduce((a,[n,r])=>a+(+r.tx_mbit||0),0);}
function upShown(m){const wire=upTotal(m),pay=m.payload_mbit==null?null:+m.payload_mbit;return pay==null||isNaN(pay)?wire:Math.min(pay,wire);}
function sigLevel(m){const s=m.details&&m.details.send;if(!s)return null;const ups=Object.values(m.uplinks||{}).filter(Boolean),on=ups.filter(u=>u.state==="an").length;if(ups.length&&!on)return 0;let q=4;if(ups.length&&on<ups.length)q-=1;const want=(s.bitrate_kbps||0)/1000;if(want>0){const r=upTotal(m)/want,backlog=s.snd_buf_ms!=null&&s.snd_buf_ms>300;if(r<.1)q-=2;else if(backlog&&r<.5)q-=2;else if(backlog&&r<.8)q-=1;}
if(s.rtt_ms!=null){if(s.rtt_ms>1000)q-=2;else if(s.rtt_ms>400)q-=1;}
if(s.snd_buf_ms!=null){if(s.snd_buf_ms>3000)q-=2;else if(s.snd_buf_ms>1500)q-=1;}
return Math.max(0,Math.min(4,q));}
let tickOk=0;const sigPts=[],SIG_WIN=30000;function sigNice(v){for(const c of[5000,10000,15000,20000,30000,50000,100000])if(v*1.05<=c)return c;return Math.ceil(v/10000)*10000;}
function sigDraw(){const line=$("net_line");if(!line||!sigPts.length)return;const now=Date.now(),cut=now-SIG_WIN;while(sigPts.length>1&&sigPts[1].t<=cut)sigPts.shift();const pts=sigPts.map(p=>({t:Math.max(cut,p.t),v:p.v})),FLOOR=0,ceil=sigNice(Math.max(...pts.map(p=>p.v)));const plot=pts.length===1?[{t:Math.max(cut,now-2000),v:pts[0].v},pts[0]]:pts,mb=v=>(v/1000).toLocaleString(undefined,{maximumFractionDigits:1});$("sg_val").textContent=mb(pts[pts.length-1].v);$("sg_max").textContent=mb(ceil);$("sg_mid").textContent=mb((FLOOR+ceil)/2);$("sg_min").textContent=mb(FLOOR);line.setAttribute("points",plot.map(p=>(Math.min(80,(p.t-cut)/SIG_WIN*80)).toFixed(1)+","+(19-(Math.max(FLOOR,Math.min(ceil,p.v))-FLOOR)/(ceil-FLOOR)*18).toFixed(1)).join(" "));}
setInterval(()=>{if(document.hidden)return;const el=$("net_sig");if(!el)return;const stale=tickOk>0&&Date.now()-tickOk>8000;el.classList.toggle("stale",stale);if(stale){$("sg_val").textContent="–";el.setAttribute("title","Verbindung: keine Messwerte von der Box");}else sigDraw();},500);function paintSig(m){const el=$("net_sig");if(!el)return;const q=sigLevel(m),sd=m.details&&m.details.send;sigPts.push({t:Date.now(),v:q==null?0:Math.max(0,upShown(m)*1000)});sigDraw();el.classList.remove("ok","warn","crit");if(q!=null)el.classList.add(q>=3?"ok":q===2?"warn":"crit");const tip=q==null?"Verbindung: keine Messung (es wird nicht gesendet)":q>=3?"Verbindung: gut":q===2?"Verbindung: mäßig":"Verbindung: schlecht";el.setAttribute("title",tip);el.setAttribute("aria-label",tip);}
async function tick(){try{const ac=typeof AbortController!=="undefined"?new AbortController():
10BC8
null,to=setTimeout(()=>{if(ac)ac.abort();},8000);let resp;try{resp=await fetch("/api/metrics",ac?{signal:ac.signal}:undefined);}finally{clearTimeout(to);}
if(resp.status===401){if(!pbUpdating)location.reload();return;}
const m=await resp.json();tickOk=Date.now();connNames=m.conn_names||connNames;renderDetails(m);paintSig(m);$("cpu").textContent=fmt(m.cpu.avg,0);if($("cpumax"))$("cpumax").textContent=m.cpu.max==null?"":`höchster Kern ${fmt(m.cpu.max,0)} %`;$("cpubar").style.width=(m.cpu.avg||0)+"%";$("temp").textContent=fmt(m.temp_c,0);{const lt=$("livetemp"),tc=m.temp_c;lt.hidden=tc==null;if(tc!=null){lt.textContent=`${fmt(tc,0)} °C`;lt.style.color=tc>=80?"var(--crit)":(tc>=70?"var(--warn)":"");}}
$("fanline").hidden=m.fan_pct==null;if(m.fan_pct!=null)$("fan").textContent=fmt(m.fan_pct,0);$("mem").textContent=fmt(m.mem.used_pct,0);$("membar").style.width=(m.mem.used_pct||0)+"%";$("memfree").textContent=(m.mem.avail_mb/1024).toFixed(1)+" GiB";const upl=((srtlaData&&srtlaData.settings)||{}).uplinks||[];let nets=Object.entries(m.net);const sel=nets.filter(([n])=>upl.includes(n));if(sel.length)nets=sel;{const tot=upShown(m),el=$("liveup");el.hidden=!(sendData&&sendData.active);el.title=m.payload_mbit==null?"Gesamter Upload über die gewählten Netze":"Nutzdaten der Sendung (Video und Ton, ohne Kopfdaten und Neuübertragungen)";el.innerHTML=`${fmt(tot)} <small>Mbit/s ↑</small>`;el.style.color=tot>=1.5?"var(--ok)":(tot>=0.5?"var(--warn)":"var(--crit)");}
const upRow=(nm,tx,rx,cls)=>`<div class="row${cls||""}"><span class="nm">${nm}</span><span class="num"><b>${fmt(tx)}</b></span><span class="unit u">Mbit/s ↑</span><span class="sep">·</span><span class="num">${fmt(rx)}</span><span class="unit u">↓</span></div>`;let netHtml=nets.map(([n,r])=>upRow(upDot(m.uplinks&&m.uplinks[n])+esc(ifd(n)),r.tx_mbit,r.rx_mbit)).join("");if(nets.length>1){const sum=k=>nets.reduce((a,[n,r])=>a+(+r[k]||0),0);netHtml+=upRow(`<b title="Summe über ${nets.length} Netze, auf der Leitung (mit Kopfdaten und Neuübertragungen)">Summe</b> <span class="unit">(${nets.length})</span>`,sum("tx_mbit"),sum("rx_mbit")," sum");}
$("net").classList.toggle("hasrows",!!netHtml);$("net").innerHTML=netHtml||'<span class="muted">keine Daten</span>';setRates(m.net);renderCams(m.cameras);renderCamLights(m.cameras);paintCamDots(m.cameras);if(due("net","c_cams",15000))netRender();if(due("ua","netcard",20000))uaLoad();if(due("dji","c_cams",20000))await djiLoad();if(due("upd","updcard",60000)){try{const ur=await fetch("/api/update");if(ur.ok)renderUpdate(await ur.json());}catch(e){}}
lastAlerts=m.alerts||[];paintHealth();}catch(e){}}
(function(){const OLD="pb_deck",wrap=$("deck_wrap"),msg=$("deck_msg"),inU=$("deck_in"),inW=$("deck_w"),inH=$("deck_h"),inMW=$("deck_mw"),inMH=$("deck_mh"),box=$("deck_optbox"),gear=$("deck_opt");
const clean=v=>{v=String(v||"").trim();if(!v)return"";if(v.length>500)return null;let u;try{u=new URL(v);}catch(e){return null;}return u.protocol==="https:"&&u.hostname&&!u.username&&!u.password?u.href:null;};
let saved={url:"",width:600,height:600,mobile_width:390,mobile_height:600},shownKey=null,loaded=false,fit=null;
const key=()=>saved.url;
const mob=()=>window.pbViews&&window.pbViews.used()==="mobile",dim=()=>mob()?{w:saved.mobile_width,h:saved.mobile_height}:{w:saved.width,h:saved.height};
function layout(){if(!fit)return;const d=dim(),avail=wrap.clientWidth||fit.box.parentNode.clientWidth||d.w,r=avail/d.w,s=r>=.85?1:r,vw=s<1?d.w:avail;
fit.box.style.height=Math.round(d.h*s)+"px";fit.f.style.width=vw+"px";fit.f.style.height=d.h+"px";fit.f.style.transform=s<1?"scale("+s+")":"";}
function render(force){const k=key();if(!force&&k===shownKey)return;shownKey=k;wrap.textContent="";fit=null;
if(!loaded){msg.hidden=false;msg.textContent="Wird geladen …";return;}
if(!saved.url){msg.hidden=false;msg.textContent="Deck-Adresse über ⚙ eintragen.";return;}
msg.hidden=true;const bx=document.createElement("div");bx.className="deckfit";const f=document.createElement("iframe");f.src=saved.url;f.title="Streamerbot Deck";f.loading="lazy";f.referrerPolicy="no-referrer";f.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-modals");f.setAttribute("allow","fullscreen");bx.appendChild(f);wrap.appendChild(bx);fit={box:bx,f:f};layout();}
function fill(){inU.value=saved.url;inW.value=saved.width;inH.value=saved.height;inMW.value=saved.mobile_width;inMH.value=saved.mobile_height;}
function take(d){if(!d||typeof d!=="object")return;saved={url:d.url||"",width:+d.width||600,height:+d.height||600,mobile_width:+d.mobile_width||390,mobile_height:+d.mobile_height||600};if(box.hidden)fill();layout();}
async function load(){try{take(await srtlaCall("GET","/api/deck"));if(!loaded){loaded=true;let old="";try{old=clean(localStorage.getItem(OLD))||"";}catch(e){}
if(old&&!saved.url){try{take(await srtlaCall("POST","/api/deck",{url:old}));}catch(e){}}
try{localStorage.removeItem(OLD);}catch(e){}if(!saved.url){box.hidden=false;gear.setAttribute("aria-expanded","true");fill();}}
render();}catch(e){if(!loaded){msg.hidden=false;msg.textContent="Das Deck konnte nicht von der Box geladen werden.";}}}
gear.addEventListener("click",()=>{box.hidden=!box.hidden;gear.setAttribute("aria-expanded",box.hidden?"false":"true");if(!box.hidden){fill();inU.focus();}});
const save=async()=>{const u=clean(inU.value);if(u===null){msg.hidden=false;msg.textContent="Die Adresse muss mit https:// beginnen.";return;}
try{take(await srtlaCall("POST","/api/deck",{url:u,width:+inW.value||600,height:+inH.value||600,mobile_width:+inMW.value||390,mobile_height:+inMH.value||600}));}catch(e){msg.hidden=false;msg.textContent="Nicht auf der Box gespeichert: "+(e&&e.message||"keine Verbindung");return;}
box.hidden=true;gear.setAttribute("aria-expanded","false");render(true);};
$("deck_save").addEventListener("click",save);for(const x of[inU,inW,inH,inMW,inMH])x.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();save();}});
if(window.ResizeObserver)new ResizeObserver(layout).observe(wrap);else window.addEventListener("resize",layout);document.addEventListener("pb-design-changed",layout);
document.addEventListener("visibilitychange",()=>{if(!document.hidden)load();});setInterval(()=>{if(!document.hidden&&box.hidden)load();},20000);
setTimeout(load,300);})();(function(){const KEY="pb_awake",btn=$("pb_awake");let on=false,lock=null,vid=null,tick=null;try{on=localStorage.getItem(KEY)==="1";}catch(e){}
function viaVideo(start){if(!start){if(vid){vid.pause();vid.srcObject=null;vid.remove();vid=null;}clearInterval(tick);return true;}
if(vid)return true;try{const c=document.createElement("canvas");c.width=c.height=2;const x=c.getContext("2d");x.fillStyle="#000";tick=setInterval(()=>{x.fillRect(0,0,2,2);},1000);const v=document.createElement("video");v.muted=true;v.setAttribute("playsinline","");v.style.cssText="position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;left:0;top:0";v.srcObject=c.captureStream(1);document.body.appendChild(v);vid=v;v.play().catch(()=>{});return true;}catch(e){return false;}}
let acquiring=false;async function acquire(){if(!on||document.hidden||lock||vid||acquiring)return;acquiring=true;try{if(navigator.wakeLock){const s=await navigator.wakeLock.request("screen");if(!on||lock){try{s.release();}catch(e){}return;}
lock=s;s.addEventListener("release",()=>{if(lock===s)lock=null;});return;}}catch(e){lock=null;}
finally{acquiring=false;}
if(on&&!viaVideo(true)){btn.title="Geht in diesem Browser nicht";}}
function release(){try{if(lock)lock.release();}catch(e){}lock=null;viaVideo(false);}
function paint(){btn.setAttribute("aria-pressed",String(on));}
btn.addEventListener("click",()=>{on=!on;try{localStorage.setItem(KEY,on?"1":"0");}catch(e){}paint();if(on)acquire();else release();});document.addEventListener("visibilitychange",()=>{if(document.hidden){lock=null;viaVideo(false);}else acquire();});paint();acquire();document.addEventListener("click",()=>{if(on&&!lock&&!vid)acquire();},{once:false});})();(function(){const card=$("c_prev"),btn=$("prev_btn"),img=$("prev_img"),vid=$("prev_vid"),msg=$("prev_msg"),box=$("prev_box"),keep=$("prev_always");let on=false,timer=0,wait=0,gen=0,poll=0,abortCtl=null,urlObj="",keepTimer=0,dogTimer=0,mode="",ctlTimer=0;const WHY={off:"Die Vorschau gibt es nur, während die Box sendet.",busy:"Es laufen schon so viele Vorschauen, wie die Box zugleich zeigen kann.","kein-bild":"Es kommt kein Bild. Läuft die Sendung?",extern:"Die Vorschau ist von außen nicht freigegeben. Sie lässt sich in dieser Karte („Vorschau“) erlauben.",missing:"Die Vorschau ist auf dieser Box nicht verfügbar.",gst:"Die Vorschau ist auf dieser Box nicht verfügbar.",root:"Die Vorschau ist auf dieser Box nicht verfügbar.",capture:"Die Vorschau ist auf dieser Box nicht verfügbar."};const store=(k,v)=>{try{if(v===undefined)return localStorage.getItem(k);if(v===null)localStorage.removeItem(k);else localStorage.setItem(k,v);}catch(e){}return null;};const always=()=>keep.checked;const keepRow=document.querySelector(".prevkeep"),gear=$("prev_set");let gearOpen=false,hintTimer=0;function syncKeep(){const ext=$("pa_ext").checked;gear.hidden=!(keep.checked||ext);if(!keep.checked&&!ext)gearOpen=false;keepRow.hidden=keep.checked&&!gearOpen;$("pa_box").hidden=ext&&!gearOpen;gear.setAttribute("aria-expanded",String(gearOpen));}
gear.addEventListener("click",e=>{e.preventDefault();gearOpen=!gearOpen;syncKeep();if(gearOpen)card.open=true;});window.pbPrevSync=close=>{if(close)gearOpen=false;syncKeep();};const running=()=>always()&&store("pb_prev_run")==="1";function closeVideo(){clearInterval(keepTimer);clearTimeout(dogTimer);if(abortCtl){try{abortCtl.abort();}catch(e){}abortCtl=null;}
try{vid.pause();}catch(e){}vid.removeAttribute("src");try{vid.srcObject=null;}catch(e){}try{vid.load();}catch(e){}
if(urlObj){try{URL.revokeObjectURL(urlObj);}catch(e){}urlObj="";}
vid.hidden=true;}
function halt(){on=false;gen++;clearTimeout(timer);clearInterval(poll);clearTimeout(hintTimer);mode="";card.classList.remove("prevrun","showctl");closeVideo();img.removeAttribute("src");img.hidden=true;box.hidden=true;}
function stop(text,more){halt();clearTimeout(wait);wait=0;btn.textContent=more?"Weiter ansehen":"Start";btn.disabled=false;msg.textContent=text||"";}
function waiting(){halt();clearTimeout(wait);wait=0;btn.textContent="Stop";btn.disabled=false;if(document.hidden){msg.textContent="";return;}
msg.textContent="Die Vorschau startet wieder, sobald die Box sendet.";wait=setTimeout(()=>{wait=0;if(running()&&!on)start(true);},10000);}
async function start(auto){const my=++gen;clearTimeout(timer);clearTimeout(wait);wait=0;btn.disabled=true;if(!auto)msg.textContent="Vorschau wird geöffnet …";card.open=true;let st;try{const r=await fetch("/api/preview");st=await r.json();}catch(e){st={available:false,why:"missing"};}
if(my!==gen)return;if(!st.available){if(running()&&(st.why==="off"||st.why==="kein-bild"))return waiting();return stop(WHY[st.why]||WHY.missing);}
on=true;btn.disabled=false;btn.textContent="Stop";msg.textContent="";card.classList.add("prevrun");clearTimeout(hintTimer);if(st.origin==="extern"&&!auto){const hint="Vorschau von außen: klein, damit der Upload für die Sendung bleibt.";msg.textContent=hint;hintTimer=setTimeout(()=>{if(msg.textContent===hint)msg.textContent="";},15000);}
playStream(my);if(!always())timer=setTimeout(()=>stop("Die Vorschau ist nach 10 Minuten beendet.",true),Math.max(5,(st.max_seconds||600)-3)*1000);clearInterval(poll);poll=setInterval(async()=>{const mine=gen;let st2;try{st2=await(await fetch("/api/preview")).json();}catch(e){return;}
if(mine!==gen||!on||st2.available||st2.why!=="off")return;if(running())waiting();else stop(WHY.off);},10000);}
async function ended(why0){if(!on)return;const my=gen;let why=why0||"kein-bild";if(!why0){try{const st=await(await fetch("/api/preview")).json();if(!st.available)why=st.why;}catch(e){why="missing";}}
if(my!==gen||!on)return;if(running()&&(why==="off"||why==="kein-bild"))waiting();else stop(WHY[why]||WHY["kein-bild"]);}
img.addEventListener("error",()=>ended());const MSC=window.ManagedMediaSource||window.MediaSource;const canMse=()=>!!(MSC&&typeof MSC.isTypeSupported==="function"&&MSC.isTypeSupported('video/mp4; codecs="avc1.4d401f"')&&window.fetch&&window.ReadableStream&&window.AbortController);const u32=(b,i)=>((b[i]<<24)|(b[i+1]<<16)|(b[i+2]<<8)|b[i+3])>>>0;const join=(a,b)=>{const o=new Uint8Array(a.length+b.length);o.set(a,0);o.set(b,a.length);return o;};const codecOf=moov=>{for(let i=0;i+8<moov.length;i++){if(moov[i]===0x61&&moov[i+1]===0x76&&moov[i+2]===0x63&&moov[i+3]===0x43){const h=n=>("0"+n.toString(16)).slice(-2);return"avc1."+h(moov[i+5])+h(moov[i+6])+h(moov[i+7]);}}
return"avc1.4d401f";};const query=()=>"&fps=30&w=640"+(always()?"&long=1":"")+"&t="+Date.now();function playMjpeg(){mode="mjpeg";closeVideo();vid.hidden=true;img.hidden=false;box.hidden=false;img.src="/api/preview/stream?fmt=mjpeg"+query();}
async function playMp4(my){mode="mp4";img.hidden=true;vid.hidden=false;box.hidden=false;const ctl=new AbortController();abortCtl=ctl;const ms=new MSC();let sb=null,moof=null,got=false,buf=new Uint8Array(0);const init=[],queue=[];vid.muted=true;vid.playsInline=true;vid.disableRemotePlayback=true;if(window.ManagedMediaSource&&MSC===window.ManagedMediaSource)vid.srcObject=ms;else{urlObj=URL.createObjectURL(ms);vid.src=urlObj;}
await new Promise((ok,no)=>{ms.addEventListener("sourceopen",ok,{once:true});setTimeout(()=>no(new Error("sourceopen")),4000);});if(my!==gen)return;const res=await fetch("/api/preview/stream?fmt=mp4"+query(),{signal:ctl.signal,cache:"no-store"});if(!res.ok||!res.body){const j=await res.json().catch(()=>({}));const e=new Error(j.error||"http");e.why=j.error;throw e;}
const pump=()=>{if(!sb||sb.updating||!queue.length)return;try{sb.appendBuffer(queue.shift());}catch(e){ctl.abort();}};keepTimer=setInterval(()=>{try{if(vid.buffered.length){const end=vid.buffered.end(vid.buffered.length-1);if(end-vid.currentTime>0.8)vid.currentTime=end-0.2;if(vid.paused&&got)vid.play().catch(()=>{});if(sb&&!sb.updating){const s0=vid.buffered.start(0);if(vid.currentTime-s0>8)sb.remove(s0,vid.currentTime-4);}}}catch(e){}},500);dogTimer=setTimeout(()=>{if(my===gen&&on&&mode==="mp4"&&!(vid.currentTime>0)){closeVideo();playMjpeg();}},8000);vid.addEventListener("error",()=>{if(my===gen&&on&&mode==="mp4"){closeVideo();playMjpeg();}},{once:true});const reader=res.body.getReader();for(;;){const r=await reader.read();if(r.done)break;if(my!==gen)return;buf=join(buf,r.value);while(buf.length>=8){const size=u32(buf,0);if(size<8||buf.length<size)break;const t=String.fromCharCode(buf[4],buf[5],buf[6],buf[7]),part=buf.slice(0,size);buf=buf.slice(size);if(t==="ftyp"||t==="moov"){init.push(part);if(t==="moov"){sb=ms.addSourceBuffer('video/mp4; codecs="'+codecOf(part)+'"');sb.addEventListener("updateend",pump);queue.push(init.reduce(join));pump();}}
else if(t==="moof")moof=part;else if(t==="mdat"&&moof){queue.push(join(moof,part));moof=null;pump();if(!got){got=true;vid.play().catch(()=>{});}}}}}
async function playStream(my){if(!canMse())return playMjpeg();try{await playMp4(my);}
catch(e){if(my!==gen||!on||(e&&e.code===20&&mode!=="mp4"))return;if(e&&e.why)return ended(e.why);if(mode==="mp4"&&!(vid.currentTime>0)){closeVideo();return playMjpeg();}
return ended();}
if(my===gen&&on&&mode==="mp4")ended();}
function showCtl(){card.classList.add("showctl");clearTimeout(ctlTimer);ctlTimer=setTimeout(()=>card.classList.remove("showctl"),10000);}
card.addEventListener("pointermove",e=>{if(e.pointerType==="mouse"&&on)showCtl();});box.addEventListener("click",()=>{if(on)showCtl();});function userStop(){store("pb_prev_run",null);stop();}
btn.addEventListener("click",e=>{e.preventDefault();if(on||wait)userStop();else{if(always())store("pb_prev_run","1");start();}});keep.addEventListener("change",()=>{store("pb_prev_always",keep.checked?"1":null);gearOpen=false;syncKeep();if(keep.checked){if(on){store("pb_prev_run","1");start(true);}}
else{store("pb_prev_run",null);if(wait){stop();}else if(on){clearTimeout(timer);timer=setTimeout(()=>stop("Die Vorschau ist nach 10 Minuten beendet.",true),597000);}}});$("prev_help_btn").addEventListener("click",e=>{e.preventDefault();const h=$("prev_help");h.hidden=!h.hidden;$("prev_help_btn").setAttribute("aria-expanded",String(!h.hidden));if(!h.hidden)card.open=true;});card.addEventListener("toggle",()=>{if(!card.open&&(on||wait))userStop();});new MutationObserver(()=>{if((on||wait)&&card.hasAttribute("data-pbhide"))userStop();}).observe(card,{attributes:true,attributeFilter:["data-pbhide"]});document.addEventListener("visibilitychange",()=>{if(document.hidden){if(on||wait){if(running())waiting();else stop();}}
else if(running()&&!on)start(true);});addEventListener("pagehide",()=>{if(on)halt();});try{keep.checked=store("pb_prev_always")==="1";}catch(e){}
syncKeep();if(running()&&!document.hidden)setTimeout(()=>{if(running()&&!on)start(true);},1500);})();(function(){const card=$("c_report"),title=$("rp_title"),desc=$("rp_desc"),go=$("rp_go"),msg=$("rp_msg"),out=$("rp_out"),list=$("rp_cases"),logrow=$("rp_logrow");let version="",now=0,win=1800,nowAt=0;const kind=()=>document.querySelector('input[name="rp_kind"]:checked').value;const NEW="https://github.com/IRL4YOU/irl4you-pip/issues/new",MAXURL=7000,ALPH="ABCDEFGHJKMNPQRSTUVWXYZ23456789";const newId=()=>{const a=new Uint8Array(6);(window.crypto||window.msCrypto).getRandomValues(a);return"IRL-"+Array.from(a,x=>ALPH[x%ALPH.length]).join("");};const clean=t=>String(t||"").replace(/\s+/g," ").trim().slice(0,100);let draft={id:"",saved:false,title:""};function sync(){const f=kind()==="feature";logrow.hidden=f;desc.placeholder=f?"Beschreibung (Pflicht)":"Beschreibung (freiwillig)";}
document.querySelectorAll('input[name="rp_kind"]').forEach(r=>r.addEventListener("change",sync));sync();const when=t=>{const d=new Date(t*1000);return d.toLocaleDateString()+" "+String(d.getHours()).padStart(2,"0")+":"+String(d.getMinutes()).padStart(2,"0");};function paint(cases,issues){list.textContent="";if(!cases.length){const p=document.createElement("div");p.className="muted";p.textContent="Noch keine Fälle.";list.appendChild(p);return;}
const age=now+(Date.now()-nowAt)/1000;for(const c of cases){const r=document.createElement("div");r.className="rpcase"+(c.done?" done":"");const d=document.createElement("span");d.className="rpd";d.textContent=when(c.t);r.appendChild(d);const kd=document.createElement("span");kd.className="rpd";kd.textContent=c.kind==="feature"?"Wunsch":"Problem";r.appendChild(kd);const id=document.createElement("code");id.textContent=c.id;id.translate=false;r.appendChild(id);const t=document.createElement("span");t.className="rpt";t.textContent=c.title;t.translate=false;r.appendChild(t);const a=document.createElement("a");a.href=(issues||"https://github.com/IRL4YOU/irl4you-pip/issues")+"?q="+encodeURIComponent("is:issue "+c.id);a.target="_blank";a.rel="noopener noreferrer";a.textContent="Auf GitHub suchen";r.appendChild(a);const b=document.createElement("button");b.type="button";b.className="sec";b.classList.add("small");b.dataset.id=c.id;b.dataset.done=c.done?"0":"1";b.textContent=c.done?"Wieder öffnen":"Erledigt";r.appendChild(b);if(age-c.t<=win){const x=document.createElement("button");x.type="button";x.className="sec";x.classList.add("small");x.dataset.id=c.id;x.dataset.del="1";x.textContent="Entfernen";x.title="Nur in den ersten 30 Minuten nach dem Anlegen möglich: Danach bleibt der Eintrag erhalten.";r.appendChild(x);}
list.appendChild(r);}}
async function load(){try{const d=await srtlaCall("GET","/api/support");version=d.version||"";now=d.now||0;win=d.delete_window||1800;nowAt=Date.now();paint(d.cases||[],d.issues);}catch(e){list.textContent="";}}
list.addEventListener("click",async e=>{const b=e.target.closest("button[data-id]");if(!b)return;try{await srtlaCall("POST","/api/support",b.dataset.del?{action:"delete",id:b.dataset.id}:{action:"done",id:b.dataset.id,done:b.dataset.done==="1"});msg.textContent="";}catch(x){msg.textContent="Fehler: "+x.message;}
load();});function compose(){const f=kind()==="feature",t=clean(title.value),body0=desc.value.trim();if(t.length<3){msg.textContent="Bitte einen Titel eintragen (mindestens 3 Zeichen).";return null;}
if(f&&!body0){msg.textContent="Bei einem Wunsch ist die Beschreibung Pflicht.";return null;}
if(!draft.id||(draft.saved&&t!==draft.title))draft={id:newId(),saved:false,title:""};const id=draft.id,ttl=(f?"[FEATURE] ":"[ISSUE] ")+t+" ("+id+")";let body=(body0?body0+"\n\n":"")+"---\nBox-Version: "+(version||"?")+"\nSupport-Nummer: "+id+(!f&&$("rp_log").checked?"\nProtokoll: als Datei angehängt":""),short=false;let url=NEW+"?title="+encodeURIComponent(ttl)+"&body="+encodeURIComponent(body);while(url.length>MAXURL&&body.length>200){body=body.slice(0,Math.floor(body.length*0.8));short=true;url=NEW+"?title="+encodeURIComponent(ttl)+"&body="+encodeURIComponent(body+"\n[gekürzt]");}
return{f,t,id,url,short};}
function show(c){out.textContent="";out.hidden=false;const a=document.createElement("a");a.className="rpgo";a.href=c.url;a.target="_blank";a.rel="noopener noreferrer";a.textContent="Formular auf GitHub öffnen";out.appendChild(a);const p=document.createElement("div");p.textContent="Das Formular öffnet in einem neuen Tab. Dort bei GitHub anmelden und unten auf „Submit new issue“ klicken."+(c.short?" Die Beschreibung war für den Link zu lang und ist gekürzt: bitte auf GitHub vervollständigen.":"");out.appendChild(p);const q=document.createElement("div");q.appendChild(document.createTextNode("Deine Support-Nummer: "));const k=document.createElement("code");k.textContent=c.id;q.appendChild(k);out.appendChild(q);const lm=document.createElement("div");lm.className="muted";out.appendChild(lm);a.addEventListener("click",ev=>{const n=compose();if(!n){ev.preventDefault();return;}
a.href=n.url;if(!draft.saved){draft.saved=true;draft.title=n.t;srtlaCall("POST","/api/support",{action:"create",kind:n.f?"feature":"issue",title:n.t,id:n.id}).then(load,()=>{});if(!n.f&&$("rp_log").checked)logsDownloadTo(go,lm).then(()=>{lm.textContent+=" Datei auf GitHub ins Textfeld ziehen.";});}});}
go.addEventListener("click",()=>{msg.textContent="";out.hidden=true;if(draft.saved)draft={id:"",saved:false,title:""};const c=compose();if(c)show(c);});card.addEventListener("toggle",()=>{if(card.open)load();});})();(function(){const on=$("na_on"),ack=$("na_ack"),ackRow=$("na_ack_row"),card=$("na_card"),addr=$("na_addr"),prefix=$("na_prefix"),info=$("na_info"),err=$("na_err"),save=$("na_save"),box=$("opt_netaddr");let last=null,ready=false;function render(d){last=d;err.textContent="";card.textContent="";for(const c of d.cards||[]){const o=document.createElement("option");o.value=c.mac;o.textContent=c.name+" · "+c.mac+(c.addrs&&c.addrs.length?" · "+c.addrs.join(", "):"");o.translate=false;card.appendChild(o);}
if(d.mac&&![...card.options].some(o=>o.value===d.mac)){const o=document.createElement("option");o.value=d.mac;o.textContent=d.mac;o.translate=false;card.appendChild(o);}
if(d.mac)card.value=d.mac;if(document.activeElement!==on)on.checked=!!d.enabled;if(d.enabled&&document.activeElement!==addr){addr.value=d.addr||"";prefix.value=d.prefix||24;}
ready=!!d.installed&&(d.cards||[]).length>0;gate();info.textContent=!d.installed?"Kommt mit dem nächsten Software-Update.":(!(d.cards||[]).length?"Keine LAN-Netzkarte gefunden.":(d.enabled?(d.present?`Aktiv: ${d.addr}/${d.prefix}`:"Eingestellt, aber gerade nicht auf der Karte. Sie wird gesetzt, sobald die Karte hochfährt."):"Aus. Die Box nutzt nur die Adresse vom Router."));}
function gate(){ackRow.hidden=!on.checked;save.disabled=!ready||(on.checked&&!ack.checked);}
on.addEventListener("change",gate);ack.addEventListener("change",gate);async function load(){try{render(await srtlaCall("GET","/api/netaddr"));}catch(e){}}
save.addEventListener("click",async()=>{err.textContent="";save.disabled=true;try{const body=on.checked?{enable:true,ack:ack.checked,mac:card.value,addr:addr.value.trim(),prefix:parseInt(prefix.value,10)}:{enable:false};render(await srtlaCall("POST","/api/netaddr",body));ack.checked=false;gate();}catch(x){err.textContent=x.message;gate();}});box.addEventListener("toggle",()=>{if(box.open)load();});})();(function(){const card=$("c_ctl"),list=$("ctl_list");let sig="",data=null,busy=false;function build(d){const fnOpts='<option value="">–</option>'+d.functions.map(f=>`<option value="${esc(f.id)}">${esc(f.name)}</option>`).join("");list.innerHTML=d.controllers.map(c=>`<div class="ctlhd" data-addr="${esc(c.addr)}"><b>${esc(c.name)}</b><span class="muted ctlst"></span></div>`+
(c.assumed&&c.keys.length?'<div class="ph" style="margin:0 0 4px">Der Controller war noch nicht verbunden. Gezeigt wird die übliche Gamepad-Belegung. Sobald er verbunden ist, erscheinen seine echten Tasten, Ihre Auswahl bleibt erhalten.</div>':"")+
(c.keys.length?c.keys.map(k=>`<div class="ctlrow" data-addr="${esc(c.addr)}" data-code="${k.code}"><span class="ctlkey">${esc(k.name)}</span><select aria-label="${esc(k.name)}">${fnOpts}</select></div>`).join(""):'<div class="muted" style="margin:6px 0">Noch keine Tasten bekannt. Controller einschalten und eine Taste drücken, dann erscheint die Liste.</div>')).join("");}
function paint(d){d.controllers.forEach(c=>{const hd=list.querySelector(`.ctlhd[data-addr="${c.addr}"] .ctlst`);if(hd)hd.textContent=c.present?"verbunden":"getrennt";c.keys.forEach(k=>{const row=list.querySelector(`.ctlrow[data-addr="${c.addr}"][data-code="${k.code}"]`);if(!row)return;const sel=row.querySelector("select"),v=c.map[String(k.code)]||"";if(sel.value!==v&&document.activeElement!==sel)sel.value=v;});});const l=d.last||{};list.querySelectorAll(".ctlrow.hit").forEach(r=>r.classList.remove("hit"));if(l.age!=null&&l.age<2){const r=list.querySelector(`.ctlrow[data-addr="${l.addr}"][data-code="${l.code}"]`);if(r)r.classList.add("hit");}
$("ctl_err").textContent=d.error||"";}
async function poll(){if(busy||document.hidden)return;busy=true;try{const d=await srtlaCall("GET","/api/controller-keys");data=d;const show=d.controllers.length>0;if(card.hidden===show){card.hidden=!show;document.dispatchEvent(new Event("pb-menus-changed"));}
const s=JSON.stringify([d.functions.map(f=>f.id),d.controllers.map(c=>[c.addr,c.name,c.assumed,c.keys.map(k=>k.code)])]);if(s!==sig&&!list.contains(document.activeElement)){sig=s;build(d);}
if(s===sig)paint(d);}catch(e){}busy=false;}
list.addEventListener("change",async e=>{const sel=e.target.closest("select"),row=e.target.closest(".ctlrow");if(!sel||!row)return;try{await srtlaCall("POST","/api/controller-keys",{addr:row.dataset.addr,code:+row.dataset.code,fn:sel.value});$("ctl_err").textContent="";}
catch(err){$("ctl_err").textContent=err.message||"Fehler";}poll();});let n=0;poll();setInterval(()=>{n++;if(!card.hidden&&card.open||n%7===0)poll();},700);})();(function(){const KEY="pb_cards_open";let st={};try{st=JSON.parse(localStorage.getItem(KEY)||"{}")||{};}catch(e){st={};}
const cards=[...document.querySelectorAll("details.card[id]")];cards.forEach(d=>{if(d.id in st)d.open=!!st[d.id];d.addEventListener("toggle",()=>{st[d.id]=d.open;try{localStorage.setItem(KEY,JSON.stringify(st));}catch(e){}});});$("fold_all").addEventListener("click",()=>cards.forEach(d=>d.open=false));$("unfold_all").addEventListener("click",()=>cards.forEach(d=>d.open=true));$("fold_toggle").addEventListener("click",()=>{const any=cards.some(d=>d.open);cards.forEach(d=>d.open=!any);});})();const LOG_NAME={sparsam:"Sparsam",ausfuehrlich:"Ausführlich"};async function logLoad(){let d;try{d=await srtlaCall("GET","/api/logmode");}catch(e){return;}
document.querySelectorAll('input[name="logmode"]').forEach(r=>{r.checked=r.value===d.mode;r.disabled=!d.helper_installed;});$("log_badge").textContent=LOG_NAME[d.mode]||"–";logModeNow=d.mode;if(!d.helper_installed)$("logmsg").textContent="Der Helfer ist noch nicht installiert. Er kommt mit dem nächsten Software-Update (oder install.sh erneut ausführen).";try{const b=await srtlaCall("GET","/api/logs");$("lg_dl").disabled=!b.helper_installed;if(!b.helper_installed)$("lg_msg").textContent="Kommt mit dem nächsten Software-Update.";}catch(e){}}
let logModeNow="";const pad2=n=>String(n).padStart(2,"0");async function logsDownloadTo(btn,msg){const wait=ms=>new Promise(r=>setTimeout(r,ms));btn.disabled=true;msg.textContent="Wird gesammelt …";try{let st=await srtlaCall("POST","/api/logs",{action:"collect"});const t0=Date.now();while(st.state!=="done"){if(st.state==="error")throw new Error(st.message||"unbekannter Fehler");if(Date.now()-t0>170000)throw new Error("Zeitüberschreitung beim Sammeln");await wait(1500);try{st=await srtlaCall("GET","/api/logs");}catch(e){}}
const r=await fetch("/api/logs/file");if(!r.ok)throw new Error("Datei konnte nicht geladen werden");const blob=await r.blob(),now=new Date();const name="irl4you-protokolle-"+now.getFullYear()+pad2(now.getMonth()+1)+pad2(now.getDate())+"-"+pad2(now.getHours())+pad2(now.getMinutes())+".txt";const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),20000);const off=-now.getTimezoneOffset()/60;msg.textContent=`Heruntergeladen: ${name} (${Math.max(1,Math.round(blob.size/1024))} KB). `+(off?`Zeiten in der Datei sind UTC (bei dir UTC${off>0?"+":""}${off}).`:"Zeiten in der Datei sind UTC.")
+(logModeNow==="sparsam"?" Im Modus „Sparsam“ fehlt die Zeit vor dem letzten Start der Box.":"");}catch(x){msg.textContent="Fehler: "+x.message;}
btn.disabled=false;}
const logsDownload=()=>logsDownloadTo($("lg_dl"),$("lg_msg"));$("lg_dl").addEventListener("click",logsDownload);document.querySelectorAll('input[name="logmode"]').forEach(r=>r.addEventListener("change",async()=>{if(!r.checked)return;$("logmsg").textContent="Wird umgestellt …";try{await srtlaCall("POST","/api/logmode",{mode:r.value});}catch(x){$("logmsg").textContent=x.message;logLoad();return;}
setTimeout(async()=>{await logLoad();$("logmsg").textContent=r.value==="sparsam"?"Umgestellt: Protokolle nur noch im Arbeitsspeicher.":"Umgestellt: Protokolle werden dauerhaft gespeichert.";},3000);}));$("c_logs").addEventListener("toggle",()=>{if($("c_logs").open)logLoad();});logLoad();const THEME_ICONS={dark:'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>',light:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg>'};function curTheme(){return document.documentElement.getAttribute("data-theme")==="light"?"light":"dark";}
function setTheme(t,save){document.documentElement.setAttribute("data-theme",t);if(save){try{localStorage.setItem("pb_theme",t);}catch(e){}}
const b=$("theme_btn");b.innerHTML=THEME_ICONS[t];b.title=t==="dark"?"Auf hell wechseln":"Auf dunkel wechseln";b.setAttribute("aria-label",b.title);const m=document.querySelector('meta[name="theme-color"]');if(m)m.content=getComputedStyle(document.documentElement).getPropertyValue("--bg").trim();frameBg();}
function frameBg(){if(typeof window!=="undefined"&&window.top!==window){try{top.document.body.style.background=getComputedStyle(document.body).backgroundColor;}catch(e){}}}
$("theme_btn").addEventListener("click",()=>setTheme(curTheme()==="dark"?"light":"dark",true));setTheme(curTheme(),false);(function(){const KEY="pb_layout",VKEY="pb_views",MKEY="pb_view_mode",main=document.querySelector("main");const blank=()=>({order:[],hidden:[],design:""});const ids=a=>(Array.isArray(a)?a:[]).filter(x=>typeof x==="string");const norm=v=>({order:ids(v&&v.order),hidden:ids(v&&v.hidden),design:(v&&(v.design==="klar"||v.design==="kompakt"||v.design==="kompakt-std"||v.design==="kontrast"||v.design==="cockpit"||v.design==="mischpult"))?v.design:""});const views={desktop:blank(),mobile:blank()};let mode="auto",edit="",legacy=false,saveT=0,savePending=new Set(),loadedAt=0;try{const c=JSON.parse(localStorage.getItem(VKEY)||"null");if(c&&c.desktop&&c.mobile){views.desktop=norm(c.desktop);views.mobile=norm(c.mobile);}
else{const o=JSON.parse(localStorage.getItem(KEY)||"{}")||{},d=localStorage.getItem("pb_design")||"";views.desktop=norm(Object.assign({},o,{design:d}));views.mobile=norm(Object.assign({},o,{design:d}));legacy=true;}}catch(e){}
try{const m=localStorage.getItem(MKEY);if(m==="desktop"||m==="mobile")mode=m;}catch(e){}
const modeCookie=()=>{try{document.cookie="pb_view_mode="+(mode==="mobile"?"mobile; max-age=31536000":"; max-age=0")+"; path=/; SameSite=Lax";}catch(e){}};modeCookie();
const phoneQ=window.matchMedia?matchMedia("(max-width:620px)"):null;const used=()=>mode==="desktop"||mode==="mobile"?mode:(phoneQ&&phoneQ.matches?"mobile":"desktop");const other=p=>p==="desktop"?"mobile":"desktop";const cache=()=>{try{localStorage.setItem(VKEY,JSON.stringify(views));}catch(e){}};const allCards=()=>[...main.children].filter(el=>el.matches("details.card[id]"));const movables=()=>allCards().filter(el=>!el.hidden);const DEFAULT=allCards().map(e=>e.id);const hideable=id=>id!=="c_layout"&&id!=="livecard";const label=el=>{const s=el.querySelector(":scope>summary");if(!s)return el.id;const h=s.querySelector(".sumh");if(h){const c=h.cloneNode(true);c.querySelectorAll("button,.badge,.ibtn").forEach(x=>x.remove());return c.textContent.trim();}
let t="";s.childNodes.forEach(n=>{if(n.nodeType===3)t+=n.textContent;});return t.trim()||el.id;};const orderOf=v=>{const res=v.order.filter(id=>DEFAULT.includes(id));DEFAULT.forEach((id,i)=>{if(res.includes(id))return;let k=i-1;while(k>=0&&!res.includes(DEFAULT[k]))k--;res.splice(k<0?0:res.indexOf(DEFAULT[k])+1,0,id);});return res;};function applyOrder(){orderOf(views[used()]).forEach(id=>main.appendChild($(id)));}
function applyHidden(){const st=views[used()];document.querySelectorAll("[data-pbhide]").forEach(e=>e.removeAttribute("data-pbhide"));st.hidden.forEach(id=>{const e=document.getElementById(id);if(e&&hideable(id))e.setAttribute("data-pbhide","");});document.documentElement.classList.toggle("nohelp",st.hidden.includes("hlp_btn"));const compact=st.hidden.includes("heads");document.documentElement.classList.toggle("nohead",compact);if(compact)["c_chat","c_prev","c_deck"].forEach(id=>{const e=document.getElementById(id);if(e)e.open=true;});buildOrder();}
function applyDesign(){const id=views[used()].design,r=document.documentElement;if(id)r.setAttribute("data-design",id);else r.removeAttribute("data-design");try{if(id)localStorage.setItem("pb_design",id);else localStorage.removeItem("pb_design");}catch(e){}
setTheme(curTheme(),false);document.dispatchEvent(new Event("pb-design-changed"));}
function applyAll(){applyOrder();applyHidden();applyDesign();buildList();buildCtl();}
function msg(t,bad){const e=$("vw_msg");if(!e)return;e.textContent=t||"";e.style.color=bad?"var(--crit)":"";}
function save(p){cache();savePending.add(p);clearTimeout(saveT);saveT=setTimeout(async()=>{const todo=[...savePending];savePending.clear();try{for(const q of todo)await srtlaCall("POST","/api/layout",{profile:q,order:views[q].order,hidden:views[q].hidden,design:views[q].design});msg("✓ Gespeichert (gilt für alle Geräte)");setTimeout(()=>{const e=$("vw_msg");if(e&&e.textContent.startsWith("✓"))e.textContent="";},3000);}catch(x){todo.forEach(q=>savePending.add(q));msg("Nicht auf der Box gespeichert: "+(x&&x.message||"keine Verbindung")+" (gilt vorerst nur in diesem Browser)",true);}},500);}
async function load(){if(savePending.size)return;try{let r=await srtlaCall("GET","/api/layout");if(!r.set){r=await srtlaCall("POST","/api/layout",{init:{desktop:views.desktop,mobile:views.mobile}});}
const n={desktop:norm(r.desktop),mobile:norm(r.mobile)};if(JSON.stringify(n)!==JSON.stringify(views)){views.desktop=n.desktop;views.mobile=n.mobile;cache();applyAll();}
else if(legacy){cache();}
legacy=false;loadedAt=Date.now();}catch(e){}}
function buildCtl(){const now=$("view_now");if(!now)return;const u=used(),n=window.innerWidth;now.textContent=`Dieses Gerät zeigt gerade die Ansicht ${u==="mobile"?"Mobil":"Desktop"} (${mode==="auto"?"automatisch, Fensterbreite "+n+" Pixel":"von Hand gewählt"}).`;$("view_mode").value=mode;for(const p of["desktop","mobile"])$("vw_"+p).setAttribute("aria-pressed",String(edit===p));$("vw_copy").textContent=edit==="desktop"?"Von Mobil übernehmen":"Von Desktop übernehmen";}
function buildList(){const box=$("lay_list"),v=views[edit];box.textContent="";const row=(g,id,txt,sub)=>{const l=document.createElement("label");l.className="optrow"+(sub?" sub":"");const c=document.createElement("input");c.type="checkbox";c.dataset.id=id;c.checked=!v.hidden.includes(id);l.appendChild(c);const s=document.createElement("span");s.textContent=txt;l.appendChild(s);g.appendChild(l);};movables().forEach(el=>{const g=document.createElement("div");g.className="optgrp";row(g,el.id,label(el),false);if(el.id==="c_layout"){const c=g.querySelector("input");c.checked=true;c.disabled=true;g.firstChild.classList.add("fixed");["opt_design","opt_order"].forEach(id=>{const sub=$(id);if(sub)row(g,id,label(sub),true);});}
else if(hideable(el.id))el.querySelectorAll("details.subsec[id],#statdet").forEach(sub=>row(g,sub.id,label(sub),true));box.appendChild(g);});{const g=document.createElement("div");g.className="optgrp";row(g,"sm_btn","Streammodus",false);box.appendChild(g);}
{const g=document.createElement("div");g.className="optgrp";row(g,"hlp_btn","Hilfe-Knöpfe (i)",false);box.appendChild(g);}
{const g=document.createElement("div");g.className="optgrp";row(g,"heads","Überschriften von Chat und Vorschau",false);const b=document.createElement("button");b.type="button";b.className="helpbtn";b.textContent="i";b.setAttribute("aria-label","Erklärung zeigen");b.setAttribute("aria-expanded","false");g.firstChild.appendChild(b);const h=document.createElement("div");h.className="ph";h.hidden=true;h.textContent="Blendet bei Chat und Vorschau die Überschrift und die Kopfzeile aus, um Platz zu sparen. Die Knöpfe (Einstellungen des Chats, Start und Stop der Vorschau) bleiben als kleine Symbole oben rechts über dem Inhalt.";b.addEventListener("click",ev=>{ev.preventDefault();h.hidden=!h.hidden;b.setAttribute("aria-expanded",String(!h.hidden));});g.appendChild(h);box.appendChild(g);}
{const g=document.createElement("div");g.className="optgrp";const l=document.createElement("label");l.className="optrow";const t=document.createElement("span");t.textContent="Fußleiste am Handy";l.appendChild(t);const sel=document.createElement("select");sel.id="foot_mode";sel.style.marginLeft="8px";for(const[v2,txt]of[["auto","Automatisch"],["always","Immer anzeigen"],["hide","Ausblenden bei Chat oder Vorschau"],["off","Immer ausblenden"]]){const o=document.createElement("option");o.value=v2;o.textContent=txt;sel.appendChild(o);}
sel.value=window.pbFootGet?window.pbFootGet():"auto";sel.addEventListener("change",()=>window.pbFootMode&&window.pbFootMode(sel.value));l.appendChild(sel);g.appendChild(l);const b=document.createElement("button");b.type="button";b.className="helpbtn";b.textContent="i";b.setAttribute("aria-label","Erklärung zeigen");b.setAttribute("aria-expanded","false");l.appendChild(b);const h=document.createElement("div");h.className="ph";h.hidden=true;h.textContent="Die Knöpfe unten am Handy (Live/Stop, Kameras, Ton). Automatisch: Sie gleiten beim Runterscrollen weg und kommen beim Hochscrollen zurück, solange Chat oder Vorschau zu sehen sind; ohne beide bleiben sie immer da. Diese Einstellung gilt nur für dieses Gerät.";g.appendChild(h);b.addEventListener("click",ev=>{ev.preventDefault();h.hidden=!h.hidden;b.setAttribute("aria-expanded",String(!h.hidden));});box.appendChild(g);}
{const g=document.createElement("div");g.className="optgrp";const l=document.createElement("label");l.className="optrow";const t=document.createElement("span");t.textContent="Anordnung der Fußleiste am Handy";l.appendChild(t);const sel=document.createElement("select");sel.id="foot_layout";sel.style.marginLeft="8px";for(const[v2,txt]of[["one","Unten, eine Zeile"],["two","Unten, zwei Zeilen (größere Knöpfe)"],["left","Links, eine Spalte"],["right","Rechts, eine Spalte"]]){const o=document.createElement("option");o.value=v2;o.textContent=txt;sel.appendChild(o);}
sel.value=window.pbFootLayoutGet?window.pbFootLayoutGet():"one";sel.addEventListener("change",()=>window.pbFootLayout&&window.pbFootLayout(sel.value));l.appendChild(sel);g.appendChild(l);const b=document.createElement("button");b.type="button";b.className="helpbtn";b.textContent="i";b.setAttribute("aria-label","Erklärung zeigen");b.setAttribute("aria-expanded","false");l.appendChild(b);const h=document.createElement("div");h.className="ph";h.hidden=true;h.textContent="Eine Zeile ist die bisherige Fußleiste. Zwei Zeilen machen sie größer (für große, gut treffbare Knöpfe): Live und Ton gehen über beide Zeilen, die Kameras stehen 2 × 2. Links oder Rechts stellt die Knöpfe als eine Spalte an den Rand. Diese Einstellung gilt nur für dieses Gerät.";g.appendChild(h);b.addEventListener("click",ev=>{ev.preventDefault();h.hidden=!h.hidden;b.setAttribute("aria-expanded",String(!h.hidden));});
const l2=document.createElement("label");l2.className="optrow";const c=document.createElement("input");c.type="checkbox";c.id="foot_info";c.checked=window.pbFootInfoGet?window.pbFootInfoGet():true;c.addEventListener("change",()=>window.pbFootInfo&&window.pbFootInfo(c.checked));l2.appendChild(c);const t2=document.createElement("span");t2.textContent="Info-Knopf (ⓘ) in der Fußleiste anzeigen";l2.appendChild(t2);g.appendChild(l2);box.appendChild(g);}}
function buildOrder(focus){const box=$("ord_list"),v=views[edit],all=orderOf(v).map(id=>$(id)).filter(el=>el&&!el.hidden);box.textContent="";all.forEach((el,i)=>{const r=document.createElement("div");r.className="ordrow"+(v.hidden.includes(el.id)&&hideable(el.id)?" off":"");const t=document.createElement("span");t.textContent=label(el);r.appendChild(t);const m=document.createElement("span");m.className="mvs";for(const[dir,txt,tip]of[[-1,"▲","Menü nach oben schieben"],[1,"▼","Menü nach unten schieben"]]){const b=document.createElement("button");b.type="button";b.className="mv";b.dataset.id=el.id;b.dataset.dir=dir;b.textContent=txt;b.title=tip;b.setAttribute("aria-label",tip);b.disabled=dir<0?i===0:i===all.length-1;m.appendChild(b);}
r.appendChild(m);box.appendChild(r);});if(focus){const mine=[...box.querySelectorAll("button.mv")].filter(x=>x.dataset.id===focus.id&&!x.disabled);const b=mine.find(x=>+x.dataset.dir===focus.dir)||mine[0];if(b)b.focus();}}
function move(id,dir){const v=views[edit],all=orderOf(v).filter(x=>$(x)&&!$(x).hidden),i=all.indexOf(id),j=i+dir;if(i<0||j<0||j>=all.length)return;[all[i],all[j]]=[all[j],all[i]];const rest=orderOf(v).filter(x=>!all.includes(x));v.order=all.concat(rest);save(edit);if(edit===used())applyOrder();buildOrder({id,dir});}
function setEdit(p){edit=p;buildList();buildOrder();buildCtl();document.dispatchEvent(new Event("pb-design-changed"));}
$("ord_list").addEventListener("click",ev=>{const b=ev.target.closest("button.mv[data-id]");if(!b||b.disabled)return;move(b.dataset.id,+b.dataset.dir);});$("lay_list").addEventListener("change",e=>{const c=e.target.closest("input[data-id]");if(!c)return;const h=views[edit].hidden,id=c.dataset.id,i=h.indexOf(id);if(c.checked&&i>=0)h.splice(i,1);if(!c.checked&&i<0)h.push(id);save(edit);if(edit===used())applyHidden();else buildOrder();});$("lay_showall").addEventListener("click",()=>{views[edit].hidden=[];save(edit);if(edit===used())applyHidden();buildList();buildOrder();});$("lay_reset").addEventListener("click",()=>{views[edit].order=[];save(edit);if(edit===used())applyOrder();buildOrder();});$("view_mode").addEventListener("change",e=>{mode=e.target.value;try{if(mode==="auto")localStorage.removeItem(MKEY);else localStorage.setItem(MKEY,mode);}catch(x){}
modeCookie();if(window.top!==window){try{top.location.reload();return;}catch(x){}}if(mode==="mobile"&&phoneQ&&!phoneQ.matches){location.reload();return;}
edit=used();applyAll();document.dispatchEvent(new Event("pb-design-changed"));});for(const p of["desktop","mobile"])$("vw_"+p).addEventListener("click",()=>setEdit(p));$("vw_copy").addEventListener("click",()=>{views[edit]=norm(JSON.parse(JSON.stringify(views[other(edit)])));save(edit);if(edit===used()){applyOrder();applyHidden();applyDesign();}buildList();buildOrder();buildCtl();});window.pbViews={used:()=>used(),edit:()=>edit,design:()=>views[edit].design,setDesign:id=>{views[edit].design=(id==="klar"||id==="kompakt"||id==="kompakt-std"||id==="kontrast"||id==="cockpit"||id==="mischpult")?id:"";save(edit);if(edit===used())applyDesign();}};document.addEventListener("pb-menus-changed",()=>{applyOrder();applyHidden();buildList();});if(phoneQ)phoneQ.addEventListener("change",()=>{if(mode==="auto"){applyAll();}});window.addEventListener("resize",()=>{if(mode==="auto")buildCtl();});document.addEventListener("visibilitychange",()=>{if(!document.hidden&&Date.now()-loadedAt>5000)load();});edit=used();applyOrder();applyHidden();applyDesign();buildList();buildCtl();document.addEventListener("pb-design-changed",()=>setTimeout(frameBg,50));frameBg();load();})();(function(){const DES=[{id:"",name:"Standard",text:"Das bisherige Aussehen der Oberfläche.",sw:["#0f172a","#1e293b","#06b6d4"]},{id:"klar",name:"Klar",text:"Hell, ruhig und großzügig: große Schrift, weiche Karten, viel Platz zum Antippen.",sw:["#f4f6fb","#ffffff","#2457d6"]},{id:"kompakt",name:"Kompakt",text:"Dicht und nüchtern: viele Werte auf einen Blick, wenig Abstand, besonders am Rechner praktisch.",sw:["#0b0f14","#121821","#37c8e0"]},{id:"kompakt-std",name:"Kompakt Standard",text:"Wie Kompakt (dicht, wenig Abstand), aber in den Farben des Standard-Designs.",sw:["#0f172a","#1e293b","#06b6d4"]},{id:"kontrast",name:"Kontrast Plus",text:"Höchster Kontrast (Schwarz, Weiß, Gelb), große Schrift, dicke Rahmen und große Knöpfe. Für schlechtes Sehen und helles Sonnenlicht.",sw:["#000000","#ffffff","#ffe600"]},{id:"cockpit",name:"Cockpit",text:"Dunkles Instrumentenbrett: Zahlen in Schreibmaschinenschrift, große Werte, Karten als Kacheln im Raster.",sw:["#05080b","#0b1218","#00e5ff"]},{id:"mischpult",name:"Mischpult",text:"Hardware-Optik: Karten als Geräteblenden mit Schrauben, Tasten, leuchtende LED-Punkte für Zustände und Balken wie Pegelketten.",sw:["#16181b","#25282d","#f2a900"]}];const cur=()=>window.pbViews?window.pbViews.design():"";function pick(id){window.pbViews.setDesign(id);let stored="";try{stored=localStorage.getItem("pb_theme")||"";}catch(e){}
if(stored!=="light"&&stored!=="dark")setTheme(id==="klar"?"light":"dark",false);}
const box=$("design_list");DES.forEach(d=>{const l=document.createElement("label");l.className="dchoice";const i=document.createElement("input");i.type="radio";i.name="design";i.value=d.id;i.checked=d.id===cur();l.appendChild(i);const sw=document.createElement("span");sw.className="dsw";sw.setAttribute("aria-hidden","true");d.sw.forEach(c=>{const e=document.createElement("i");e.style.background=c;sw.appendChild(e);});l.appendChild(sw);const t=document.createElement("span");t.className="dtx";const b=document.createElement("b");b.textContent=d.name;const x=document.createElement("span");x.textContent=d.text;t.appendChild(b);t.appendChild(x);l.appendChild(t);box.appendChild(l);});document.addEventListener("pb-design-changed",()=>box.querySelectorAll("input[name=design]").forEach(i=>{i.checked=i.value===cur();}));box.addEventListener("change",e=>{const i=e.target.closest("input[name=design]");if(i&&i.checked)pick(i.value);});})();const HLP_SEL=".card .ph:not([id]):not([role])";function hlpOwner(ph){const isHead=e=>!!e&&(e.tagName==="SUMMARY"||(e.classList&&e.classList.contains("sech")));if(ph.hasAttribute("data-hlp")){for(let e=ph;e&&!(e.classList&&e.classList.contains("card"));e=e.parentElement)
for(let p=e.previousElementSibling;p;p=p.previousElementSibling)if(isHead(p))return p;return null;}
let prev=ph.previousElementSibling;if(!prev&&ph.parentElement&&ph.parentElement.tagName==="DIV")prev=ph.parentElement.previousElementSibling;return isHead(prev)?prev:null;}
(function(){document.querySelectorAll(HLP_SEL).forEach(ph=>{const head=hlpOwner(ph);if(!head)return;let b=head.querySelector(":scope>.ibtn,:scope>.sumh>.ibtn");if(!b){b=document.createElement("button");b.type="button";b.className="ibtn";b.textContent="i";b.setAttribute("aria-expanded","false");b.setAttribute("aria-label","Hilfe anzeigen");b.title="Hilfe";b._hlp=[];(head.querySelector(":scope>.sumh")||head).appendChild(b);}
b._hlp.push(ph);ph.classList.add("hlp");});document.addEventListener("click",e=>{const b=e.target.closest&&e.target.closest(".ibtn");if(!b)return;e.preventDefault();e.stopPropagation();const open=b.getAttribute("aria-expanded")!=="true";b.setAttribute("aria-expanded",String(open));b.setAttribute("aria-label",open?"Hilfe ausblenden":"Hilfe anzeigen");(b._hlp||[]).forEach(p=>p.classList.toggle("open",open));},true);})();let bkDoc=null,bkInfo=null,bkSending=false,bkPw="";const BK_MIN=8;function bkPwHint(){const on=$("bk_secrets").checked;$("bk_pwlab").textContent=on?`Passwort zum Verschlüsseln (mind. ${BK_MIN} Zeichen)`:"Passwort (leer = unverschlüsselt)";}
$("bk_secrets").addEventListener("change",bkPwHint);async function bkExport(){const sec=$("bk_secrets").checked,pw=$("bk_pw").value,pw2=$("bk_pw2").value,msg=$("bk_msg"),btn=$("bk_export");if(pw!==pw2){msg.textContent="Die beiden Passwörter sind nicht gleich.";return;}
if((sec||pw)&&pw.length<BK_MIN){msg.textContent=sec?`Mit Passwörtern bitte ein Passwort eingeben (mind. ${BK_MIN} Zeichen).`:`Mindestens ${BK_MIN} Zeichen (oder leer lassen).`;return;}
btn.disabled=true;msg.textContent="Wird gesammelt und verschlüsselt …";try{const r=await srtlaCall("POST","/api/settings/export",{secrets:sec,password:pw||undefined});const blob=new Blob([JSON.stringify(r.document,null,1)],{type:"application/json"}),now=new Date();const name="irl4you-einstellungen-"+now.getFullYear()+pad2(now.getMonth()+1)+pad2(now.getDate())+"-"+pad2(now.getHours())+pad2(now.getMinutes())+".json";const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),20000);$("bk_pw").value="";$("bk_pw2").value="";msg.textContent="Heruntergeladen: "+name+" ("+Math.max(1,Math.round(blob.size/1024))+" KB). "
+(r.encrypted?"Verschlüsselt. Das Passwort wird nirgends gespeichert.":"Unverschlüsselt, ohne Passwörter.")
+((r.notes||[]).length?" Hinweis: "+r.notes.join("; "):"");}catch(x){msg.textContent="Fehler: "+x.message;}
btn.disabled=false;}
$("bk_export").addEventListener("click",bkExport);function bkReset(){bkDoc=null;bkInfo=null;bkPw="";$("bk_plan").hidden=true;setHtml($("bk_plan"),"");$("bk_ipwrow").hidden=true;$("bk_ipw").value="";}
$("bk_file").addEventListener("change",async e=>{const f=e.target.files&&e.target.files[0];bkReset();$("bk_imsg").textContent="";$("bk_reload").hidden=true;if(!f)return;if(f.size>300000){$("bk_imsg").textContent="Datei zu groß.";return;}
try{bkDoc=JSON.parse(await f.text());}catch(x){bkDoc=null;$("bk_imsg").textContent="Keine Sicherung (kein JSON).";return;}
if(bkDoc&&bkDoc.encrypted===true){$("bk_ipwrow").hidden=false;$("bk_imsg").textContent="Verschlüsselt: bitte Passwort eingeben.";$("bk_ipw").focus();return;}
bkPreview();});async function bkPreview(){const pw=$("bk_ipw").value;$("bk_imsg").textContent="Wird geprüft …";try{bkInfo=await srtlaCall("POST","/api/settings/preview",{document:bkDoc,password:pw||undefined});}
catch(x){$("bk_imsg").textContent=x.message;return;}
bkPw=pw;$("bk_ipwrow").hidden=true;$("bk_ipw").value="";$("bk_imsg").textContent="";const r=bkInfo,when=r.created?new Date(r.created):null,ts=when&&!isNaN(when)?when.toLocaleString(LOC(),{dateStyle:"medium",timeStyle:"short"}):"";const rows=r.sections.map(x=>x.ok?`<label class="logopt"><input type="checkbox" data-sec="${esc(x.id)}" checked> <b>${esc(x.label)}</b> <span class="muted">(${x.count})</span>${x.note?`<span class="ph">${esc(x.note)}</span>`:""}</label>`:`<div class="logopt bkbad"><b>${esc(x.label)}</b>: nicht lesbar, wird ausgelassen. <span class="ph">${esc(x.note)}</span></div>`).join("");setHtml($("bk_plan"),`<div class="wstat ok"><span class="wdot"></span><span class="wgrow"><span>${ts?`Sicherung vom ${esc(ts)}`:"Sicherung"}</span>${r.box_version?`<span>${`(Version ${esc(r.box_version)})`}</span>`:""}, <span>${r.encrypted?"verschlüsselt":"nicht verschlüsselt"}</span>, <span>${r.secrets?"mit Passwörtern":"ohne Passwörter"}</span></span></div>
<div class="sech">Einspielen (ersetzt die jetzigen Einstellungen)</div>${rows}
<div class="actions"><button id="bk_go" type="button" class="warnbtn"${bkSending?" disabled":""}>Ausgewählte Teile einspielen</button></div>`);$("bk_plan").hidden=false;if(bkSending)$("bk_imsg").textContent="Es wird gesendet. Bitte erst beenden.";}
$("bk_open").addEventListener("click",bkPreview);$("bk_ipw").addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();bkPreview();}});const BK_TODO="pb_bk_todo";function bkTodoState(){try{const v=JSON.parse(localStorage.getItem(BK_TODO)||"null");return v&&typeof v==="object"?v:null;}catch(e){return null;}}
function bkTodoSave(v){try{if(v)localStorage.setItem(BK_TODO,JSON.stringify(v));else localStorage.removeItem(BK_TODO);}catch(e){}}
function bkTodoRender(){const box=$("bk_todo"),st=bkTodoState();if(!box)return;box.hidden=!st;if(st)box.querySelectorAll("input[data-todo]").forEach(i=>{i.checked=!!(st.done||{})[i.dataset.todo];});}
function bkTodoShow(){if(!bkTodoState())bkTodoSave({done:{}});bkTodoRender();}
$("bk_todo").addEventListener("change",e=>{const i=e.target.closest("input[data-todo]");if(!i)return;const st=bkTodoState()||{done:{}};st.done=st.done||{};st.done[i.dataset.todo]=i.checked;const all=[...$("bk_todo").querySelectorAll("input[data-todo]")].every(x=>st.done[x.dataset.todo]);bkTodoSave(all?null:st);if(all)setTimeout(bkTodoRender,600);else bkTodoRender();});bkTodoRender();function bkResults(r){bkTodoShow();setHtml($("bk_imsg"),(r.results||[]).map(x=>`<div class="${x.ok?"bkok":"bkbad"}">${x.ok?"✓":"✗"} <b>${esc(x.label)}</b>: ${esc(x.message)}</div>`).join(""));$("bk_reload").hidden=false;[typeof srtlaLoad==="function"&&srtlaLoad,typeof pipeLoad==="function"&&pipeLoad,typeof wifiLoad==="function"&&wifiLoad,typeof djiLoad==="function"&&djiLoad].forEach(f=>{try{f&&f();}catch(e){}});}
async function bkWithProgress(fn){const t0=Date.now();progSet("bk_prog",true,3,"Der Stand davor wird gesichert",t0);const poll=setInterval(async()=>{try{const p=await srtlaCall("GET","/api/settings/progress");if(p.state==="running"&&p.total)progSet("bk_prog",true,Math.max(5,Math.round(p.done*100/p.total)),p.label?`Einspielen: ${p.label} (${p.done+1} von ${p.total})`:"Der Stand davor wird gesichert");}catch(e){}},600);try{return await fn();}
finally{clearInterval(poll);progSet("bk_prog",true,100,"Fertig");setTimeout(()=>progSet("bk_prog",false),2500);}}
document.addEventListener("click",async e=>{if(e.target.id==="bk_go"){const secs=[...document.querySelectorAll("#bk_plan input[data-sec]:checked")].map(i=>i.dataset.sec);if(!secs.length){$("bk_imsg").textContent="Bitte mindestens einen Teil wählen.";return;}
if(!confirm("Die gewählten Teile ersetzen die jetzigen Einstellungen. Der Stand davor wird gesichert.\n\nEinspielen?"))return;e.target.disabled=true;$("bk_imsg").textContent="Wird eingespielt …";try{const r=await bkWithProgress(()=>srtlaCall("POST","/api/settings/import",{document:bkDoc,password:bkPw||undefined,sections:secs}));bkResults(r);bkReset();$("bk_file").value="";}
catch(x){$("bk_imsg").textContent="Fehler: "+x.message;e.target.disabled=false;}
bkLoad();}});$("bk_restore").addEventListener("click",async()=>{if(!confirm("Stand vor dem letzten Einspielen zurückholen? Die jetzigen Einstellungen werden ersetzt."))return;$("bk_imsg").textContent="Wird zurückgeholt …";try{bkResults(await bkWithProgress(()=>srtlaCall("POST","/api/settings/restore",{})));}catch(x){$("bk_imsg").textContent="Fehler: "+x.message;}
bkLoad();});$("bk_reload").addEventListener("click",()=>location.reload());async function bkLoad(){try{const d=await srtlaCall("GET","/api/settings");bkSending=!!d.sending;$("bk_restore").hidden=!d.has_backup;const go=$("bk_go");if(go)go.disabled=bkSending;}catch(e){}}
$("c_bk").addEventListener("toggle",()=>{if($("c_bk").open)bkLoad();});bkPwHint();let devData=null,devTimer=null,devPwShown=false;function devPwButton(){$("dev_pw").textContent=devPwShown?"Passwort ausblenden":"Passwort anzeigen";}
async function devLoad(){let d;try{d=await srtlaCall("GET","/api/developer");}catch(e){return;}
devData=d;const busy=d.state==="working",usable=d.helper_installed&&!busy;$("dev_badge").textContent=!d.available?"–":(d.active?"SSH an":"SSH aus");$("dev_badge").className="badge "+(d.active?"attn":"");$("dev_state").className="wstat"+(d.active?" ok":"");$("dev_text").textContent=!d.available?"Auf dieser Box gibt es keinen SSH-Dienst.":(d.active?"SSH aktiv":"SSH ist ausgeschaltet")+(d.user?" · "+`Benutzer: „${d.user}“`:"");const t=$("dev_toggle");t.textContent=d.active?"SSH ausschalten":"SSH einschalten";t.className=d.active?"dangerbtn":"warnbtn";t.disabled=!usable||!d.available;$("dev_reset").disabled=!usable||!d.can_reset;$("dev_reset").hidden=!d.can_reset;$("dev_pw").disabled=!d.helper_installed;devPwButton();if(!d.helper_installed)$("dev_msg").textContent="Der Helfer ist noch nicht installiert. Er kommt mit dem nächsten Software-Update (oder install.sh erneut ausführen).";else if(busy){$("dev_msg").textContent="Läuft …";clearTimeout(devTimer);devTimer=setTimeout(devLoad,1500);}
else $("dev_msg").textContent=d.state==="error"?(d.message||"Fehler"):"";}
async function devPost(body,wait){$("dev_msg").textContent=wait;try{await srtlaCall("POST","/api/developer",body);}catch(x){$("dev_msg").textContent=x.message;return false;}
clearTimeout(devTimer);devTimer=setTimeout(devLoad,1200);[3000,8000].forEach(ms=>setTimeout(devLoad,ms));return true;}
async function devPwShow(){const prev=devData?devData.time:0,out=$("dev_pw_out");out.textContent="";$("dev_msg").textContent="Prüfe das Passwort …";try{await srtlaCall("POST","/api/developer",{action:"check"});}catch(x){$("dev_msg").textContent=x.message;return;}
for(let i=0;i<8;i++){await new Promise(r=>setTimeout(r,800));await devLoad();if(devData&&devData.state!=="working"&&devData.time!==prev)break;}
$("dev_msg").textContent="";try{const r=await srtlaCall("GET","/api/developer/password");out.innerHTML='<div class="pwshow" title="Klicken kopiert das Passwort">'+esc(r.password)+'</div>'+
(r.state==="own"?'<div class="muted">Achtung: Das Passwort wurde danach von Hand geändert, dieses gilt vermutlich nicht mehr.</div>':'');devPwShown=true;}catch(x){$("dev_msg").textContent=x.message;devPwShown=false;}
devPwButton();}
function devPwHide(){$("dev_pw_out").textContent="";devPwShown=false;devPwButton();}
$("dev_toggle").addEventListener("click",async()=>{const start=!(devData&&devData.active);if(await devPost({action:start?"start":"stop"},start?"Schalte SSH ein …":"Schalte SSH aus …")&&start&&devPwShown)devPwHide();});$("dev_pw").addEventListener("click",()=>{if(devPwShown)devPwHide();else devPwShow();});$("dev_reset").addEventListener("click",async()=>{if(!confirm("SSH-Passwort zurücksetzen?\n\nDas alte Passwort gilt danach nicht mehr."))return;devPwHide();const prev=devData?devData.time:0;if(await devPost({action:"reset",confirm:true},"Erzeuge ein neues Passwort …")){for(let i=0;i<15;i++){await new Promise(r=>setTimeout(r,800));await devLoad();if(devData&&devData.state!=="working"&&devData.time!==prev)break;}
if(devData&&devData.state!=="error")devPwShow();}});$("c_dev").addEventListener("toggle",()=>{if($("c_dev").open)devLoad();else devPwHide();});devLoad();async function powerDo(action){let st={};try{st=await srtlaCall("GET","/api/power");}catch(e){}
if(!st.helper_installed){$("pwmsg").textContent="Der Helfer ist auf dieser Box noch nicht installiert (install.sh erneut ausführen).";return;}
const live=st.sending?"\n\nAchtung: Es wird gerade gesendet. Die Übertragung bricht dabei ab.":"";if(!confirm((action==="poweroff"?"Die Box jetzt herunterfahren?\n\nDanach ist die Box AUS und startet nicht von selbst wieder. Zum Einschalten den Strom kurz trennen oder die Ein-Taste drücken.":"Die Box jetzt neu starten?")+live))return;try{await srtlaCall("POST","/api/power",{action,confirm:true});}
catch(x){$("pwmsg").textContent=x.message;return;}
$("pwmsg").textContent=action==="poweroff"?"Die Box fährt herunter. Sobald die Lüfter stehen, ist sie aus. Diese Seite ist dann nicht mehr erreichbar.":"Die Box startet neu. Diese Seite lädt sich nach etwa einer Minute wieder.";}
$("pw_off").addEventListener("click",()=>powerDo("poweroff"));$("pw_reboot").addEventListener("click",()=>powerDo("reboot"));setSM(smStart);(async function tickLoop(){try{if(!document.hidden)await tick();}catch(e){}
setTimeout(tickLoop,2000);})();document.addEventListener("visibilitychange",()=>{if(!document.hidden){tick();sendLoad();}});</script> <script src="/chat.js"></script> </body> </html>