CUE-SZENENLISTE PRO LAYER + macOS-SUPPORT (35/35 E2E, 123 Checks gesamt)

1) Cue-/Szenenliste JE Layer (35/35 E2E):
- Cue sichern = Snapshot (Quelle, Alpha, Position, Groesse, Z-Order,
  FX1/FX2 + Intensitaeten); Name + fade_ms + follow_ms pro Cue
- GO vor/zurueck (steps), Goto (Index), Wrap-around am Listenende
- FADE: Quellwechsel = dip-to-black (Phase1 runter, Wechsel bei 0,
  Phase2 hoch; E2E: alpha 0.49 -> 0.32 -> Ziel 0.8), sonst sanfter
  Tween von Alpha + FX-Intensitaeten; laufende Fades abbrechbar
- AUTO-FOLLOW: follow_ms schaltet nach Ablauf automatisch weiter
  (E2E: Cue mit 600ms -> Wrap zum naechsten)
- DMX CUE-GO: cue_go_channel (Settings + UI), steigende Flanke
  >=128 = GO auf allen Layern; ohne Flanke kein GO (E2E: 0->1,
  1==1 bei High-Halten, neue Flanke 1->0); Sequenzfilter aktiv
- Cue-Entfernen mit Indexkorrektur; Cues in Projekten
  speicher-/ladbar (3 Cues nach Projekt-Load erhalten)
- UI: Cue-Bereich je Layer (Cue sichern / GO / Zurueck / Goto /
  Loeschen, aktiver Cue markiert, Fade+Follow-Anzeige)
- Fehlerfaelle: unbekannter Layer 400, fehlendes Medium 400,
  unbekannter Generator 400

2) macOS (Apple Silicon + Intel) mit Skript:
- setup_macos.sh: Homebrew installiert python@3.12, python-tk,
  GStreamer + alle Plugin-Sets + PyGObject; Apple-Silicon-PATH-
  Hinweis (/opt/homebrew); Erklaerung warum Homebrew statt
  gstreamer-.pkg (keine PyGObject-Bindings in .pkg)
- Bootstrap (--bootstrap): darwin-Zweig mit brew-Installationspfad
- run.py --generate-setup erzeugt jetzt 3 Skripte
  (Windows .ps1 / Linux .sh / macOS .sh); Bash-Syntax geprueft

3) Gefundene + behobene FEHLER:
- WEB-UI-ABSTURZ (echter JS-Ternary-Klammerfehler in loadServers,
  seit Verteilungs-Commit unentdeckt, weil API-Tests den Browser-
  Code nie parsen): Klammerstruktur korrigiert + JS-Syntax nun per
  node --check verifizierbar (Syntax OK) + Remote-Buttons robust
  per data-Attribute (kein Quote-Escaping mehr)
- update_layer blockierte Quellwechsel Video->Generator (Cues
  brauchen beides): Name entscheidet jetzt den Typ, Wechsel in
  beide Richtungen
- Cue-E2E-Timing: dip-to-black hat 2 Phasen (Pruefung 0.2s/0.6s),
  Auto-Follow-Index-Fix, Art-Net-Sequenznummern im Test
  inkrementierend (Sequenzfilter verwirft Duplikate korrekt)

E2E-Bilanz: Cue 35/35 + Regressionen Projekte 25/25,
Verteilung 33/33, App 14/14, FX 16/16 = 123 Checks gruen
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 13:34:43 +02:00
parent 7e191ff6c9
commit 02765e6b34
9 changed files with 770 additions and 27 deletions
+37 -4
View File
@@ -147,6 +147,7 @@ footer{background:var(--pan);padding:5px 16px;font-size:11px;color:var(--dim);bo
<div class="frow"><label>Kanal Playback</label><input type="number" id="s-artnet-playback" min="1" max="512"></div>
<div class="frow"><label>Kanal Retrigger</label><input type="number" id="s-artnet-retrigger" min="1" max="512"></div>
<div class="frow"><label>Kanal Blackout</label><input type="number" id="s-artnet-blackout" min="1" max="512"></div>
<div class="frow"><label>Kanal Cue-GO</label><input type="number" id="s-artnet-cuego" min="1" max="512"><span class="hint">Flanke ≥128 = nächster Cue je Layer</span></div>
<div class="frow"><label>Signalverlust</label><select id="s-artnet-policy"><option value="hold">Letzten Zustand halten</option><option value="fade_black">Auf schwarz fallen</option></select>
<div class="frow"><label>Timeout Signalverlust</label><input type="number" id="s-artnet-timeout"> s</div>
</div>
@@ -217,7 +218,16 @@ function renderLayerEdit(ls){const e=$('ledit');
<div class="frow"><label>FX1 Intensität</label><input type="range" id="fx1i-${L.id}" min="0" max="100" value="${Math.round((L.fx1_intensity??0.5)*100)}" oninput="api('layers/update',{id:${L.id},fx1_intensity:this.value/100})"></div>
<div class="frow"><label>Effekt 2</label><select id="fx2-${L.id}" onchange="api('layers/update',{id:${L.id},fx2:this.value||null})"></select></div>
<div class="frow"><label>FX2 Intensität</label><input type="range" id="fx2i-${L.id}" min="0" max="100" value="${Math.round((L.fx2_intensity??0.5)*100)}" oninput="api('layers/update',{id:${L.id},fx2_intensity:this.value/100})"></div>
</div>`).join('')||'<p style="color:var(--dim)">Keine aktiven Layer. Medien-Tab öffnen und Datei als Layer hinzufügen.</p>'}}
<h2 style="margin-top:10px">Cue-Liste (Szenen)</h2>
<div id="cues-${L.id}"></div>
<div class="row">
<button class="acc" onclick="addCue(${L.id})">Cue sichern</button>
<button onclick="cueGo(${L.id},1)">GO ▶</button>
<button onclick="cueGo(${L.id},-1)">◀ Zurück</button>
</div>
</div>`).join('')||'<p style="color:var(--dim)">Keine aktiven Layer. Medien-Tab öffnen und Datei als Layer hinzufügen.</p>'}
ls.forEach(renderCues);
}
// Medien
async function loadMedia(){try{const d=await(await fetch('/api/media')).json();
@@ -260,7 +270,7 @@ async function loadSettings(){try{const s=await(await fetch('/api/settings')).js
$('s-artnet-enabled').checked=s.artnet.enabled;$('s-artnet-port').value=s.artnet.listen_port;
$('s-artnet-universes').value=s.artnet.universes.join(',');
$('s-artnet-master').value=s.artnet.master_channel;$('s-artnet-layer').value=s.artnet.layer_start_channel;
$('s-artnet-playback').value=s.artnet.playback_channel;$('s-artnet-retrigger').value=s.artnet.retrigger_channel;$('s-artnet-blackout').value=s.artnet.blackout_channel;
$('s-artnet-playback').value=s.artnet.playback_channel;$('s-artnet-retrigger').value=s.artnet.retrigger_channel;$('s-artnet-blackout').value=s.artnet.blackout_channel;$('s-artnet-cuego').value=s.artnet.cue_go_channel??'';
$('s-artnet-policy').value=s.artnet.signal_loss_policy;$('s-artnet-timeout').value=s.artnet.signal_loss_timeout_s;
$('s-engine-max').value=s.engine.max_layers;$('s-engine-loop').checked=s.engine.loop_default;$('s-output-fs').checked=s.output.fullscreen;
$('s-media-dir').value=s.media.dir;$('s-media-max').value=s.media.max_upload_mb;
@@ -271,7 +281,7 @@ async function saveSettings(){const body={
artnet:{enabled:$('s-artnet-enabled').checked,listen_port:+$('s-artnet-port').value,
universes:$('s-artnet-universes').value.split(',').map(x=>parseInt(x.trim())).filter(x=>!isNaN(x)),
master_channel:+$('s-artnet-master').value,layer_start_channel:+$('s-artnet-layer').value,
playback_channel:+$('s-artnet-playback').value,retrigger_channel:+$('s-artnet-retrigger').value,blackout_channel:+$('s-artnet-blackout').value,
playback_channel:+$('s-artnet-playback').value,retrigger_channel:+$('s-artnet-retrigger').value,blackout_channel:+$('s-artnet-blackout').value,cue_go_channel:+$('s-artnet-cuego').value||null,
signal_loss_policy:$('s-artnet-policy').value,signal_loss_timeout_s:+$('s-artnet-timeout').value},
engine:{max_layers:+$('s-engine-max').value,loop_default:$('s-engine-loop').checked,autostart_project:$('s-autostart').value||null},
output:{fullscreen:$('s-output-fs').checked},
@@ -311,10 +321,33 @@ async function loadServers(){try{const d=await(await fetch('/api/servers')).json
// Medien-Tab: Remote-Medien
const rg=$('rgrid');if(rg)rg.innerHTML=servers.map(S=>
`<div class="setgrp"><h3>${S.name} <span class="badge${S.online?'':' err'}">${S.online?'online':'offline'}</span></h3>`+
(S.online?(S.items||[]).map(m=>`<div class="frow"><label>${m.name} <small style="color:var(--dim)">${fmtSize(m.size)}</small></label><button class="acc" onclick="importRemote('${S.name}','${m.name.replace(/'/g,"\\'")}')">Ins Archiv ziehen</button></div>`).join('')||'<p style="color:var(--dim);font-size:11px">Keine Medien auf dem Server.</p>'):'')+'</div>'
(S.online?(S.items||[]).map(m=>`<div class="frow"><label>${m.name} <small style="color:var(--dim)">${fmtSize(m.size)}</small></label><button class="acc" data-srv="${S.name}" data-m="${m.name}" onclick="importRemote(this.dataset.srv,this.dataset.m)">Ins Archiv ziehen</button></div>`).join('')||'<p style="color:var(--dim);font-size:11px">Keine Medien auf dem Server.</p>':'')+'</div>'
).join('')||'<p style="color:var(--dim);font-size:11px">Noch keine Server. In den Einstellungen registrieren.</p>';
}catch(e){}}
// ---------- Cue-Listen (Szenen) pro Layer ----------
function cueRow(L,c,i){return `<div class="frow" style="padding:2px 0">
<label>${i+1}. ${c.name} <small style="color:var(--dim)">${c.fade_ms}ms Fade${c.follow_ms?' · Auto '+Math.round(c.follow_ms/1000)+'s':''}</small></label>
${i===L.cue_index?'<span class="badge">aktiv</span>':''}
<button onclick="cueGoto(${L.id},${i})">Goto</button>
<button class="danger" onclick="delCue(${L.id},${c.cue_id})">✕</button></div>`}
function renderCues(L){const box=$('cues-'+L.id);if(!box)return;
const ls=(L.cues||[]);
if(ls.length===0){box.innerHTML='<p style="color:var(--dim);font-size:11px">Noch keine Cues. Button unten sichert den aktuellen Zustand als Cue.</p>';return}
box.innerHTML=ls.map((c,i)=>cueRow(L,c,i)).join('')}
async function addCue(id){const nm=prompt('Cue-Name:','Cue');if(nm===null)return;
const fm=prompt('Fade-Zeit in ms (0 = harter Schnitt):','500');if(fm===null)return;
const fo=prompt('Auto-Follow in ms (0 = aus, sonst Weiter nach Ablauf):','0');if(fo===null)return;
const d=await api('layers/cues/add',{id,name:nm||undefined,fade_ms:+fm||0,follow_ms:+fo||0});
if(d.cue)toast('Cue gesichert: '+d.cue.name);else if(d.error)toast(d.error,true)}
async function cueGo(id,steps){const d=await api('layers/cues/go',{id,steps});
if(d.cue)toast('GO → '+d.cue);else if(d.error)toast(d.error,true)}
async function cueGoto(id,index){const d=await api('layers/cues/goto',{id,index});
if(d.cue)toast('Goto: '+d.cue)}
async function delCue(id,cueId){if(!confirm('Cue entfernen?'))return;
await api('layers/cues/remove',{id,cue_id:cueId});
poll()}
let FX=[];
async function loadFx(){try{FX=await(await fetch('/api/fx')).json();fillFxSelects()}catch(e){}}
function fillFxSelects(){document.querySelectorAll('select[id^="fx1-"],select[id^="fx2-"]').forEach(sel=>{