PROJEKTE: Speichern/Laden/Autostart (25/25 E2E inkl. Server-Neustart)

- hms_app/projects.py: ProjectStore mit atomarem Speichern
  (.hms.json in projects/), Format-Kennung, Namens-Sanitize
  (Pfad-Traversal-Schutz, Leerzeichen erlaubt), Liste/Loeschen
- engine.py: export_state() (Layer mit Alpha/Pos/Groesse/Z/FX +
  Intensitaeten + Master) und load_state() (ersetzt alle Layer,
  fehlende Medien werden uebersprungen und gemeldet, Werte geclamped,
  ungueltige FX-IDs verworfen)
- server.py: /api/projects (GET Liste + Autostart),
  POST projects/save|load|delete|autostart; Loeschen des
  Autostart-Projekts raeumt config.json automatisch auf
- config.py: engine.autostart_project (validiert, optional)
- run.py: laedt Autostart-Projekt beim Programmstart
- ui.py: neuer Reiter 'Projekte': Name eingeben + Speichern,
  Tabelle mit Laden/Autostart/Loeschen, Autostart-Badge

E2E-BEWEIS (25/25 PASS, mit ECHTEM Server-Neustart):
1 Server startet · 2 Upload · 3 Layer · 4 FX/Alpha/Pos gesetzt ·
5 Projekt 'Show A' gespeichert · 6 Traversal-Name sanitizet zu 'evil' ·
7 Liste · 8 Layerzahl · 9 Zustand geleert (master=0.1) ·
10 Projekt geladen · 11 Layer wiederhergestellt ·
12 Alpha 0.7 wiederhergestellt · 13 FX1 blur 0.8 wiederhergestellt ·
14 Position x=50 y=20 wiederhergestellt · 15 Master 0.9 ·
16 Engine rendert · 17 Autostart gesetzt ·
18 Server NEU GESTARTET · 19 Autostart: Layer+FX automatisch geladen ·
20 Autostart: Engine rendert (60 Frames) · 21 config.json Autostart ·
22 fehlendes Projekt 404 · 23 geloescht ·
24 Autostart automatisch geleert · 25 config.json geleert

Regressionen: App-E2E 14/14 (app=0) + FX-E2E 16/16 (fx=0) unveraendert gruen
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 12:55:50 +02:00
parent 10b988c33a
commit 70502458d8
8 changed files with 469 additions and 2 deletions
+21 -1
View File
@@ -70,6 +70,7 @@ footer{background:var(--pan);padding:5px 16px;font-size:11px;color:var(--dim);bo
<button data-p="live" class="on">Live</button>
<button data-p="media">Medien</button>
<button data-p="layers">Layer</button>
<button data-p="projects">Projekte</button>
<button data-p="settings">Einstellungen</button>
</div>
</header>
@@ -112,6 +113,15 @@ footer{background:var(--pan);padding:5px 16px;font-size:11px;color:var(--dim);bo
<div id="ledit"></div>
</div>
<div id="page-projects" class="page">
<div class="row" style="max-width:480px">
<input type="text" id="projname" placeholder="Projektname…" style="flex:1;width:auto">
<button class="acc" onclick="saveProject()">Speichern</button>
</div>
<p style="color:var(--dim);font-size:11px;margin:6px 0 14px">Speichert alle Layer inkl. Effekte, Position, Alpha und Master. Autostart lädt das Projekt beim Programmstart automatisch.</p>
<table id="projtable"><thead><tr><th>Projekt</th><th>Layer</th><th>Gespeichert</th><th>Aktionen</th></tr></thead><tbody></tbody></table>
</div>
<div id="page-settings" class="page">
<div class="setgrp"><h3>Web-UI</h3>
<div class="frow"><label>Port</label><input type="number" id="s-web-port"></div>
@@ -159,7 +169,7 @@ async function api(path,body){try{const r=await fetch('/api/'+path,{method:'POST
// Tabs
document.querySelectorAll('#tabbar button').forEach(b=>b.onclick=()=>showTab(b.dataset.p));
function showTab(p){document.querySelectorAll('.page').forEach(x=>x.classList.remove('on'));$('page-'+p).classList.add('on');document.querySelectorAll('#tabbar button').forEach(x=>x.classList.toggle('on',x.dataset.p===p));if(p==='media')loadMedia();if(p==='settings')loadSettings();}
function showTab(p){document.querySelectorAll('.page').forEach(x=>x.classList.remove('on'));$('page-'+p).classList.add('on');document.querySelectorAll('#tabbar button').forEach(x=>x.classList.toggle('on',x.dataset.p===p));if(p==='media')loadMedia();if(p==='settings')loadSettings();if(p==='projects')loadProjects();}
// Live-Steuerung
$('master').oninput=e=>{api('master',{intensity:e.target.value/100});$('mv').textContent=e.target.value+'%'};
@@ -257,6 +267,16 @@ async function saveSettings(){const body={
if(d.saved){$('setmsg').textContent='Gespeichert ✓';setTimeout(()=>$('setmsg').textContent='',2500);
if(d.hint)toast(d.hint)}}
async function loadProjects(){try{const d=await(await fetch('/api/projects')).json();
const tb=document.querySelector('#projtable tbody');
tb.innerHTML=(d.items||[]).map(p=>`<tr><td>${p.name}${d.autostart===p.name?' <span class="badge">Autostart</span>':''}</td><td>${p.layers}</td><td>${p.saved_at||''}</td>
<td><button class="acc" onclick="loadProject('${p.name}')">Laden</button> <button onclick="setAutostart('${p.name}')">Autostart</button> <button class="danger" onclick="delProject('${p.name}')">✕</button></td></tr>`).join('')||'<tr><td colspan="4" style="color:var(--dim)">Noch keine Projekte gespeichert.</td></tr>'}catch(e){}}
async function saveProject(){const name=$('projname').value.trim();if(!name){toast('Bitte Projektnamen eingeben',true);return}
const d=await api('projects/save',{name});if(d.saved){toast('Projekt gespeichert: '+d.saved);$('projname').value='';loadProjects()}}
async function loadProject(name){const d=await api('projects/load',{name});if(d.loaded){toast('Projekt geladen: '+name);showTab('live')}else toast(d.error||'Laden fehlgeschlagen',true)}
async function delProject(name){if(!confirm(name+' löschen?'))return;await api('projects/delete',{name});loadProjects()}
async function setAutostart(name){const d=await api('projects/autostart',{name});if(d.autostart){toast('Autostart aktiv: '+d.autostart);loadProjects()}}
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=>{