PageSourceSearch

https://corsch.net/buero3d/betrieb.js

js corsch.net collected 2026-10-09 23:55:26 UTC 23,361 bytes, 519 lines download raw bytes

1// Live office — Etappe 3. Same contract as GET /api/backlog/buero.
2// Headless Nachweis has no adminFetch, so this module stays dark and the
3// first frame stays the literal Belegschaft from belegschaft.js.
4//
5// After the first poll the figures do what the 2D office does: sit at
6// their own desk when they work, walk the hall to a rest spot when they
7// don't, and take a seat at hausMeetingTisch when runden[0] has a voice.
8// Arms swing on the walk and type at the desk; rest pose is 0.
9
10import { setzeFigurStand } from './belegschaft.js';
11import {
12    standVon, imGespraechVon, sitzplaetzeBesprechung, wegVon,
13    wegLaenge, wegPunkt, laufDauer, blickNach, SITZ_ZAHL,
14    sitzpose, hoeheVon, laufHoehe, etageVon, armWinkel, STATION_NAME,
15} from './ort.js';
16
17const POLL_MS = 8000;
18const AM_ZIEL = 0.05;
19
20function rolleVonDaten(daten, name) {
21    return ((daten && daten.rollen) || []).find((r) => r && r.name === name) || null;
22}
23
24function rundeAmTisch(daten) {
25    const runden = Array.isArray(daten && daten.runden) ? daten.runden : [];
26    return runden.length ? runden[0] : null;
27}
28
29const HALTUNG_TEXT = { dafuer: 'dafür', dagegen: 'dagegen', unentschieden: 'unentschieden' };
30
31function vorgangDerRolle(daten, name) {
32    const eigene = ((daten && daten.vorgaenge) || []).filter((v) => v.zustaendig === name);
33    return eigene.find((x) => Number(x.zeigen) === 1)
34        || eigene.find((x) => Number(x.in_arbeit))
35        || eigene[0]
36        || null;
37}
38
39function stationText(key) {
40    if (!key) return '';
41    return STATION_NAME[key] || key;
42}
43
44function taetigkeit(daten, name, stand) {
45    if (stand === 'besprechung') {
46        const runde = rundeAmTisch(daten);
47        const stimme = ((runde && runde.stimmen) || []).find((s) => s && s.rolle === name);
48        if (stimme && stimme.haltung && HALTUNG_TEXT[stimme.haltung]) {
49            return { art: 'haltung', text: HALTUNG_TEXT[stimme.haltung] };
50        }
51        return { art: 'besprechung', text: (runde && runde.titel) || 'am Besprechungstisch' };
52    }
53    const rolle = rolleVonDaten(daten, name);
54    if (!rolle || (rolle.an_arbeit_seit_min === undefined && rolle.arbeitet_seit_min === undefined)) {
55        return null;
56    }
57    const v = vorgangDerRolle(daten, name);
58    if (rolle.an_arbeit_seit_min !== undefined) {
59        const station = stationText(v && v.station);
60        const titel = (v && v.titel) || 'arbeitet';
61        return {
62            art: 'arbeit',
63            text: station ? (station + ' · ' + titel) : titel,
64            station: (v && v.station) || '',
65            seit_min: Number(rolle.an_arbeit_seit_min),
66        };
67    }
68    return { art: 'geliefert', text: 'zuletzt geliefert' };
69}
70
71// Same words as 2D standText / blaseInhalt. Name is always on; work
72// text only when there is a real stand (never invent a sentence).
73function etikett(daten, name, stand) {
74    const t = taetigkeit(daten, name, stand);
75    if (t) return t;
76    if (stand === 'besprechung') return { art: 'besprechung', text: 'am Besprechungstisch' };
77    if (stand === 'arbeit') return { art: 'arbeit', text: 'arbeitet' };
78    return { art: 'name', text: '' };
79}
80
81function wenigBewegung() {
82    return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
83}
84
85// Steht der Besprechungstisch in dieser Szene, und ist er an? Mehr fragt
86// dieses Modul das Mesh nicht mehr. Bis zum 17.08.2026 hat es hier die
87// Position von 'hausMeetingTischTop' gelesen — das Mesh des alten Hauses,
88// das eineVilla() abschaltet und das seine Koordinate danach trotzdem
89// weiter herausgibt: getMeshByName kennt kein isEnabled. Damit sassen die
90// sechs Sitze bei z=-10,8, also 19 m vor der Suedwand im Freien, und die
91// Flurlinie gleich mit.
92//
93// Neu: der Name ist der des Hanghaus-Tisches, ein abgeschaltetes Mesh
94// zaehlt als nicht vorhanden, und die Koordinaten kommen ohnehin nicht vom
95// Mesh, sondern aus BESPRECHUNG — der Stelle, aus der auch der Tisch
96// gebaut wird. Ohne Tisch gibt es keine Besprechungssitze und die Figuren
97// bleiben, wo sie sind. Das ist der richtige Ausgang, nicht der Notausgang.
98const TISCH_MESH = 'hangBespPlatte';
99
100function tischOrt(scene) {
101    const platte = scene.getMeshByName(TISCH_MESH);
102    if (!platte) return null;
103    if (typeof platte.isEnabled === 'function' && !platte.isEnabled()) return null;
104    return { vorhanden: true };
105}
106
107export function starteBetrieb(scene) {
108    const zustand = {
109        live: false,
110        daten: null,
111        figuren: new Map(),
112        phase: new Map(),
113        labels: new Map(),
114        wege: new Map(),
115        // Auf welcher Etage jede Figur gerade steht. Aus der Welthoehe ist
116        // das nicht zurueckzurechnen — wer im Obergeschoss am Tisch sitzt,
117        // ist bei 3,49, und das ist keine Etage. Also mitgefuehrt.
118        etage: new Map(),
119        sitze: [],
120        // Namen der Figuren, deren metadata.home ohne Etage geschrieben
121        // wurde. Leer, solange die drei Schreibstellen ihr y setzen; siehe
122        // heimVon(). Der Waechter liest die Liste aus.
123        heimatOhneEtage: [],
124    };
125
126    function registriere(m) {
127        const rolle = m && m.metadata && m.metadata.rolle;
128        if (!rolle || typeof rolle !== 'string') return;
129        if (!m.name || m.name !== 'figur_' + rolle.toLowerCase()) return;
130        zustand.figuren.set(rolle, m);
131        zustand.phase.set(m, Math.random() * Math.PI * 2);
132    }
133    scene.meshes.forEach(registriere);
134    if (scene.transformNodes) scene.transformNodes.forEach(registriere);
135
136    // Keine Flurlinie mehr, die hier einmal eingefroren wird — sie haengt am
137    // Stockwerk der Figur und wird bei jedem Lauf neu geholt.
138    //
139    // Und die Sitze werden nachgezogen, solange es keine gibt: scene.js ruft
140    // starteBetrieb() auf, BEVOR es veredleSzene() aufruft, und erst
141    // veredleSzene() baut das Hanghaus. Hier stand bis zum 18.08.2026 eine
142    // Zeilennummer statt der beiden Funktionsnamen; sie war nach einem Tag
143    // falsch (1689 -> 1717). Die Reihenfolge ist die Aussage, die Zeile war
144    // nur ihr Fundort. Ein einmaliger Griff hier faende den
145    // Besprechungstisch nie und die Besprechung fiele still aus. Das Umstellen
146    // der beiden Aufrufe waere die andere Loesung — Sigrun hat am 17.08.2026
147    // gemessen, dass sie den eigentlichen Fehler nicht behebt, und eine
148    // Verdrahtung, die von der Zeilenreihenfolge in einer fremden Datei
149    // abhaengt, ist ohnehin keine.
150    function sitzeHolen() {
151        if (!zustand.sitze.length && tischOrt(scene)) {
152            zustand.sitze = sitzplaetzeBesprechung();
153        }
154        return zustand.sitze;
155    }
156    sitzeHolen();
157
158    function schicht() {
159        let el = document.getElementById('b3d-blasen');
160        if (!el && scene.getEngine().getRenderingCanvas()) {
161            const canvas = scene.getEngine().getRenderingCanvas();
162            if (!canvas.parentElement) return null;
163            el = document.createElement('div');
164            el.id = 'b3d-blasen';
165            el.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:6;overflow:hidden;';
166            canvas.parentElement.appendChild(el);
167        }
168        return el;
169    }
170
171    function blaseFuer(name, inhalt) {
172        const layer = schicht();
173        if (!layer) return;
174        let node = zustand.labels.get(name);
175        if (!inhalt) {
176            if (node) { node.remove(); zustand.labels.delete(name); }
177            return;
178        }
179        if (!node) {
180            node = document.createElement('div');
181            node.style.cssText = 'position:absolute;max-width:200px;padding:3px 8px 4px;' +
182                'border-radius:10px 10px 10px 4px;font:11px/1.3 ui-sans-serif,system-ui,sans-serif;' +
183                'background:rgba(10,14,22,0.9);color:#F4F7FF;white-space:nowrap;' +
184                'overflow:hidden;text-overflow:ellipsis;transform:translate(-50%,-110%);';
185            layer.appendChild(node);
186            zustand.labels.set(name, node);
187        }
188        const farbe = inhalt.art === 'arbeit' ? '#6FE1FF'
189            : (inhalt.art === 'besprechung' ? '#FFD76B'
190                : (inhalt.art === 'geliefert' ? '#FFD76B' : 'rgba(232,237,246,0.35)'));
191        node.style.border = '1px solid ' + farbe;
192        node.textContent = inhalt.text ? (name + ' · ' + inhalt.text) : name;
193    }
194
195    function dauerMinText(min) {
196        const n = Number(min);
197        if (!isFinite(n) || n < 0) return '';
198        if (n < 1) return 'gerade eben';
199        if (n === 1) return '1 min';
200        return Math.round(n) + ' min';
201    }
202
203    function jetztLeiste(daten) {
204        const canvas = scene.getEngine().getRenderingCanvas();
205        if (!canvas || !canvas.parentElement) return;
206        let el = document.getElementById('b3d-jetzt');
207        if (!el) {
208            el = document.createElement('div');
209            el.id = 'b3d-jetzt';
210            el.setAttribute('role', 'region');
211            el.setAttribute('aria-label', 'Jetzt im Haus — wer gerade an etwas sitzt');
212            el.style.cssText = 'position:absolute;left:10px;right:10px;top:10px;z-index:8;' +
213                'padding:7px 10px 8px;border-radius:8px;pointer-events:none;' +
214                'font:12px/1.35 ui-sans-serif,system-ui,sans-serif;' +
215                'background:rgba(8,12,18,0.82);color:#E8EDF6;' +
216                'border:1px solid rgba(111,225,255,0.28);';
217            canvas.parentElement.appendChild(el);
218        }
219        const sitzend = [];
220        zustand.figuren.forEach((mesh, name) => {
221            const t = taetigkeit(daten, name, 'arbeit');
222            if (t && t.art === 'arbeit') sitzend.push({ name, mesh, t });
223        });
224        sitzend.sort((a, b) => (a.t.seit_min || 0) - (b.t.seit_min || 0));
225        const arbeit = window.__belegschaft && window.__belegschaft.arbeit || 0;
226        const besprechung = window.__belegschaft && window.__belegschaft.besprechung || 0;
227        const ruhe = window.__belegschaft && window.__belegschaft.ruhe || 0;
228        const kopf = 'Jetzt im Haus  ·  ' +
229            (arbeit === 1 ? '1 arbeitet' : arbeit + ' arbeiten') +
230            '  ·  ' + (besprechung ? besprechung + ' in Besprechung  ·  ' : '') +
231            (ruhe === 1 ? '1 in Ruhe' : ruhe + ' in Ruhe');
232        el.textContent = '';
233        const titel = document.createElement('div');
234        titel.style.cssText = 'color:#6FE1FF;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:5px;';
235        titel.textContent = kopf;
236        el.appendChild(titel);
237        if (!sitzend.length) {
238            const leer = document.createElement('div');
239            leer.style.color = '#9aa3b5';
240            leer.textContent = 'Niemand sitzt gerade an etwas. Figuren in Ruhe stehen im Flur.';
241            el.appendChild(leer);
242            return;
243        }
244        const liste = document.createElement('div');
245        liste.style.cssText = 'display:flex;flex-wrap:wrap;gap:5px;';
246        sitzend.forEach((eintrag) => {
247            const chip = document.createElement('button');
248            chip.type = 'button';
249            chip.style.cssText = 'pointer-events:auto;cursor:pointer;max-width:240px;' +
250                'padding:3px 8px;border-radius:6px;text-align:left;color:#F4F7FF;' +
251                'background:rgba(111,225,255,0.10);border:1px solid rgba(111,225,255,0.4);' +
252                'font:11px/1.3 ui-sans-serif,system-ui,sans-serif;';
253            chip.textContent = eintrag.name + ' · ' + eintrag.t.text;
254            const seit = dauerMinText(eintrag.t.seit_min);
255            chip.title = seit ? eintrag.name + ' sitzt seit ' + seit : eintrag.name;
256            chip.addEventListener('click', () => {
257                const cam = scene.activeCamera;
258                if (!cam || !cam.setTarget) return;
259                const p = eintrag.mesh.getAbsolutePosition();
260                cam.setTarget(new BABYLON.Vector3(p.x, p.y + 1.1, p.z));
261            });
262            liste.appendChild(chip);
263        });
264        el.appendChild(liste);
265    }
266
267    function projektion(mesh) {
268        const cam = scene.activeCamera;
269        const engine = scene.getEngine();
270        if (!cam) return null;
271        const welt = mesh.getAbsolutePosition().add(new BABYLON.Vector3(0, 1.85, 0));
272        const p = BABYLON.Vector3.Project(
273            welt,
274            BABYLON.Matrix.Identity(),
275            scene.getTransformMatrix(),
276            cam.viewport.toGlobal(engine.getRenderWidth(), engine.getRenderHeight()),
277        );
278        if (p.z < 0 || p.z > 1) return null;
279        const canvas = engine.getRenderingCanvas();
280        const box = canvas.getBoundingClientRect();
281        const parent = canvas.parentElement.getBoundingClientRect();
282        return {
283            x: (p.x / engine.getRenderWidth()) * box.width + (box.left - parent.left),
284            y: (p.y / engine.getRenderHeight()) * box.height + (box.top - parent.top),
285        };
286    }
287
288    function heimVon(mesh) {
289        const home = mesh.metadata && mesh.metadata.home;
290        if (home && typeof home.y === 'number') return home;
291        if (home) {
292            // Hier stand bis zum 18.08.2026 `{ y: 0, ...home }`. Das war
293            // kein Vorgabewert, das war eine Zudeckung: zwei der drei
294            // Schreibstellen (optik.js, belegschaft.js) liessen die Etage
295            // weg, und die elf Figuren im Obergeschoss standen nur deshalb
296            // richtig, weil veredleSzene() eineVilla() VOR bauHanghaus()
297            // ruft und hanghaus.js als letzter schrieb. Ein Tausch der
298            // beiden Aufrufe haette sie ein Stockwerk fallen lassen, ohne
299            // dass irgendetwas rot geworden waere. Eine Heimat ohne Etage
300            // ist jetzt ein gemeldeter Fehler, kein Erdgeschoss: die
301            // uebrigen Angaben bleiben, die Etage wird an der Figur
302            // gemessen statt geraten, und der Name steht in
303            // zustand.heimatOhneEtage — dort holt ihn der Waechter ab.
304            if (zustand.heimatOhneEtage.indexOf(mesh.name) < 0) {
305                zustand.heimatOhneEtage.push(mesh.name);
306            }
307            return { ...home, y: etageVon(mesh.position.y) };
308        }
309        return {
310            x: mesh.position.x,
311            z: mesh.position.z,
312            // Ohne Heimatangabe ist der Platz, auf dem die Figur steht, ihre
313            // Heimat — und die Etage die, auf der sie steht.
314            y: etageVon(mesh.position.y),
315            rotationY: mesh.rotation.y,
316            doorX: mesh.position.x,
317        };
318    }
319
320    function etageDerFigur(name, mesh) {
321        if (zustand.etage.has(name)) return zustand.etage.get(name);
322        return heimVon(mesh).y || 0;
323    }
324
325    // Jedes Ziel traegt seine Etage mit. `ruhe` fuehrt seit dem 17.08.2026
326    // an den eigenen Platz zurueck und nicht mehr in eine Reihe vor dem
327    // abgerissenen Haus; wer schon dort steht, geht also keinen Schritt.
328    function zielVon(mesh, name, stand, sitz) {
329        if (stand === 'besprechung' && sitz) {
330            return {
331                x: sitz.x, z: sitz.z, y: sitz.y || 0,
332                rotationY: sitz.rotationY, alignX: sitz.alignX,
333            };
334        }
335        const home = heimVon(mesh);
336        if (stand === 'arbeit' && home.sitzX !== undefined && home.sitzZ !== undefined) {
337            return {
338                x: home.sitzX, z: home.sitzZ, y: home.y,
339                rotationY: home.rotationY, alignX: home.doorX,
340            };
341        }
342        return {
343            x: home.x, z: home.z, y: home.y,
344            rotationY: home.rotationY, alignX: home.doorX,
345        };
346    }
347
348    function setzeArme(mesh, art, t, phase) {
349        const arme = mesh && mesh.metadata && mesh.metadata.arme;
350        if (!arme || arme.length < 2) return;
351        const w = armWinkel(art, t, phase);
352        arme[0].rotation.x = w.links;
353        arme[1].rotation.x = w.rechts;
354    }
355
356    // y ist Etage PLUS Pose, nie die Pose allein. Vorher stand hier
357    // `mesh.position.y = pose.y`, also 0,34 fuer jeden Sitzenden — die elf
358    // Figuren des Obergeschosses fielen damit beim ersten Poll ins
359    // Erdgeschoss, sichtbar durch den Boden.
360    function setzen(name, mesh, ziel, stand) {
361        mesh.position.x = ziel.x;
362        mesh.position.z = ziel.z;
363        mesh.rotation.y = ziel.rotationY;
364        const pose = sitzpose(stand, false);
365        mesh.position.y = hoeheVon(ziel.y, stand, false);
366        mesh.rotation.x = pose.rotationX;
367        zustand.etage.set(name, ziel.y || 0);
368        setzeArme(mesh, stand === 'arbeit' ? 'arbeit' : (stand === 'besprechung' ? 'besprechung' : 'ruhe'), 0, 0);
369    }
370
371    function starteLauf(name, mesh, ziel, stand) {
372        const vonEtage = etageDerFigur(name, mesh);
373        const von = { x: mesh.position.x, z: mesh.position.z, y: vonEtage };
374        if (Math.hypot(von.x - ziel.x, von.z - ziel.z) < AM_ZIEL) {
375            zustand.wege.delete(name);
376            setzen(name, mesh, ziel, stand);
377            return;
378        }
379        const home = heimVon(mesh);
380        // wegVon holt die Flurlinie des Stockwerks, auf dem die Figur gerade
381        // steht — nicht die eine Linie, die frueher bei Init aus dem Tisch
382        // des alten Hauses gelesen und fuer alle neunzehn festgehalten wurde.
383        const punkte = wegVon(von, ziel, home.doorX);
384        const strecke = wegLaenge(punkte);
385        if (wenigBewegung() || strecke < AM_ZIEL) {
386            zustand.wege.delete(name);
387            setzen(name, mesh, ziel, stand);
388            return;
389        }
390        zustand.wege.set(name, {
391            punkte,
392            start: performance.now(),
393            dauer: laufDauer(strecke),
394            ziel,
395            stand,
396            vonEtage,
397            strecke,
398        });
399    }
400
401    function anwenden(daten) {
402        zustand.daten = daten;
403        zustand.live = true;
404        const gespraech = imGespraechVon(daten, sitzeHolen().length || SITZ_ZAHL);
405        let arbeit = 0;
406        let besprechung = 0;
407        zustand.figuren.forEach((mesh, name) => {
408            const stand = standVon(rolleVonDaten(daten, name), gespraech, daten);
409            const sitzIndex = gespraech.has(name) ? gespraech.get(name) : -1;
410            const sitz = sitzIndex >= 0 ? zustand.sitze[sitzIndex] : null;
411            setzeFigurStand(mesh, stand);
412            blaseFuer(name, etikett(daten, name, stand));
413            starteLauf(name, mesh, zielVon(mesh, name, stand, sitz), stand);
414            if (stand === 'arbeit') arbeit += 1;
415            else if (stand === 'besprechung') besprechung += 1;
416        });
417        window.__belegschaft = window.__belegschaft || {};
418        window.__belegschaft.live = true;
419        window.__belegschaft.arbeit = arbeit;
420        window.__belegschaft.besprechung = besprechung;
421        window.__belegschaft.ruhe = zustand.figuren.size - arbeit - besprechung;
422        jetztLeiste(daten);
423    }
424
425    async function abrufen() {
426        if (typeof window.adminFetch !== 'function') return;
427        if (document.hidden) return;
428        try {
429            const res = await window.adminFetch('/backlog/buero');
430            if (res && res.data && res.data.datenbank !== false) anwenden(res.data);
431        } catch (_) {
432            // Stay on the last good stand. A failed poll is not a black office.
433        }
434    }
435
436    scene.onBeforeRenderObservable.add(() => {
437        const jetzt = performance.now();
438        const t = jetzt / 180;
439        const gespraech = zustand.live
440            ? imGespraechVon(zustand.daten, zustand.sitze.length || SITZ_ZAHL)
441            : null;
442        zustand.figuren.forEach((mesh, name) => {
443            if (zustand.live) {
444                const weg = zustand.wege.get(name);
445                const stand = standVon(rolleVonDaten(zustand.daten, name), gespraech, zustand.daten);
446                const phase = zustand.phase.get(mesh) || 0;
447                if (weg) {
448                    const u = (jetzt - weg.start) / weg.dauer;
449                    if (u >= 1) {
450                        setzen(name, mesh, weg.ziel, weg.stand || stand);
451                        zustand.wege.delete(name);
452                    } else {
453                        const p = wegPunkt(weg.punkte, u);
454                        mesh.position.x = p.x;
455                        mesh.position.z = p.z;
456                        mesh.rotation.x = 0;
457                        // Der Schritt federt um die Etage, auf der gegangen
458                        // wird, nicht um Null.
459                        mesh.position.y = laufHoehe(weg.vonEtage, weg.ziel.y, u)
460                            + 0.03 * Math.abs(Math.sin(t * 2 + phase));
461                        if (p.dx !== 0 || p.dz !== 0) mesh.rotation.y = blickNach(p.dx, p.dz);
462                        setzeArme(mesh, 'gehen', t, phase);
463                    }
464                } else {
465                    const pose = sitzpose(stand, false);
466                    mesh.rotation.x = pose.rotationX;
467                    mesh.position.y = hoeheVon(etageDerFigur(name, mesh), stand, false)
468                        + (stand === 'arbeit' ? 0.018 * Math.sin(t + phase) : 0);
469                    const rolle = rolleVonDaten(zustand.daten, name);
470                    const tippt = stand === 'arbeit' && rolle && rolle.an_arbeit_seit_min !== undefined;
471                    setzeArme(mesh, tippt ? 'arbeit' : (stand === 'besprechung' ? 'besprechung' : 'ruhe'), t, phase);
472                }
473            }
474        });
475    });
476
477    // Die Schilder werden NACH dem Bild gesetzt, nicht davor. In
478    // onBeforeRender traegt scene.getTransformMatrix() noch die Matrix des
479    // VORIGEN Bildes — Babylon schreibt sie erst in _renderForCamera, also
480    // nach diesem Observable. Das Schild hing der Figur damit bei jedem
481    // Kameraschwenk um genau ein Bild hinterher (gemessen 17.08.2026: 1,8 px
482    // bei 0,02 rad/Bild, 7,9 px beim Reissen, 17 px bei 50 m Abstand zum
483    // Kameraziel).
484    //
485    // Die Figurenanimation bleibt bewusst oben in onBeforeRender. Wandert sie
486    // mit hierher, laufen die Figuren ein Bild zu spaet und der Versatz ist
487    // nur umgezogen, statt behoben zu sein.
488    scene.onAfterRenderObservable.add(() => {
489        zustand.figuren.forEach((mesh, name) => {
490            const label = zustand.labels.get(name);
491            if (!label) return;
492            const xy = projektion(mesh);
493            if (!xy) { label.style.display = 'none'; return; }
494            label.style.display = 'block';
495            label.style.left = Math.round(xy.x) + 'px';
496            label.style.top = Math.round(xy.y) + 'px';
497        });
498    });
499
500    // Names from the first frame (literal roster). Live poll overwrites
501    // the text; headless has no overlay parent so this stays a no-op.
502    (function seedLabels() {
503        const liste = (window.__belegschaft && window.__belegschaft.stand) || [];
504        zustand.figuren.forEach((mesh, name) => {
505            const eintrag = liste.find((s) => s && s.name === name);
506            blaseFuer(name, etikett(null, name, (eintrag && eintrag.stand) || 'ruhe'));
507        });
508    }());
509
510    // First poll after the Nachweis screenshot has already been taken
511    // (headless has no adminFetch; interactive waits a beat so __sceneReady
512    // resolves on the literal frame).
513    setTimeout(abrufen, 1200);
514    setInterval(abrufen, POLL_MS);
515
516    zustand.anwenden = anwenden;
517    window.__betrieb = zustand;
518    return zustand;
519}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.