UX Block 3: Upload-Fortschritt, Toggle-Switch, vars.yml-Download

- Upload-Fortschrittsbalken per XHR mit Progress-Event
- Checkbox-Toggle statt Ja/Nein-Select für Enabled-Feld
- vars.yml Download-Button im Provisioning-Workflow
- Alle UX-Aufgaben in TODO.md abgehakt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-23 11:06:27 +01:00
parent 62c1b8cd5c
commit fa74ceb5d8
3 changed files with 87 additions and 15 deletions

View file

@ -155,9 +155,9 @@
### Niedrige Prioritaet ### Niedrige Prioritaet
- [ ] Upload-Fortschrittsbalken in Manage-UI - [x] Upload-Fortschrittsbalken in Manage-UI
- [ ] vars.yml Download-Button in Provision-UI statt Copy-Paste - [x] vars.yml Download-Button in Provision-UI statt Copy-Paste
- [ ] Toggle-Switch statt Ja/Nein-Select fuer Enabled-Feld - [x] Toggle-Switch statt Ja/Nein-Select fuer Enabled-Feld
## Querschnittsthemen ## Querschnittsthemen

View file

@ -62,7 +62,10 @@ ansible_user: {{.SSHUser}}
screen_id: {{.Screen.Slug}} screen_id: {{.Screen.Slug}}
screen_name: "{{.Screen.Name}}" screen_name: "{{.Screen.Name}}"
screen_orientation: {{.Orientation}}</pre> screen_orientation: {{.Orientation}}</pre>
<button class="button is-small is-light copy-btn mt-2" onclick="copy('hostvars')">📋 Kopieren</button> <div class="buttons mt-2">
<button id="copy-btn-hostvars" class="button is-small is-light copy-btn" onclick="copy('hostvars', 'copy-btn-hostvars')">📋 Kopieren</button>
<button class="button is-small is-light" onclick="downloadFile(document.getElementById('hostvars').innerText, 'vars.yml')"> Als Datei herunterladen</button>
</div>
<p class="help mt-2">Tipp: <code>mkdir -p ansible/host_vars/{{.Screen.Slug}}</code></p> <p class="help mt-2">Tipp: <code>mkdir -p ansible/host_vars/{{.Screen.Slug}}</code></p>
</div> </div>
</div> </div>
@ -115,17 +118,26 @@ ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit {{.Screen.Slu
</section> </section>
<script> <script>
function copy(id) { function copy(id, btnId) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (!el) return; if (!el) return;
navigator.clipboard.writeText(el.innerText).then(function() { navigator.clipboard.writeText(el.innerText).then(function() {
var btn = el.nextElementSibling; var btn = btnId
? document.getElementById(btnId)
: el.nextElementSibling;
if (!btn) return; if (!btn) return;
var orig = btn.textContent; var orig = btn.textContent;
btn.textContent = ' Kopiert!'; btn.textContent = ' Kopiert!';
setTimeout(function() { btn.textContent = orig; }, 1500); setTimeout(function() { btn.textContent = orig; }, 1500);
}); });
} }
function downloadFile(content, filename) {
var a = document.createElement('a');
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
a.download = filename;
a.click();
}
</script> </script>
</body> </body>
</html>` </html>`
@ -581,11 +593,11 @@ document.addEventListener('keydown', function(e) {
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<label class="label is-small">Aktiv</label> <label class="label is-small">Aktiv</label>
<div class="select is-small"> <div class="control" style="padding-top:0.4rem">
<select name="enabled"> <label class="checkbox">
<option value="true"{{if .Enabled}} selected{{end}}>Ja</option> <input type="checkbox" name="enabled" value="true" {{if .Enabled}}checked{{end}}>
<option value="false"{{if not .Enabled}} selected{{end}}>Nein</option> Aktiv
</select> </label>
</div> </div>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
@ -673,7 +685,7 @@ document.addEventListener('keydown', function(e) {
</div> </div>
<div id="panel-file" class="tab-panel is-active"> <div id="panel-file" class="tab-panel is-active">
<form method="POST" action="/manage/{{.Screen.Slug}}/upload" enctype="multipart/form-data"> <form id="upload-form" method="POST" action="/manage/{{.Screen.Slug}}/upload" enctype="multipart/form-data">
<div class="columns is-vcentered"> <div class="columns is-vcentered">
<div class="column is-2"> <div class="column is-2">
<div class="field"> <div class="field">
@ -698,7 +710,7 @@ document.addEventListener('keydown', function(e) {
<div class="field"> <div class="field">
<label class="label">Datei</label> <label class="label">Datei</label>
<div class="control"> <div class="control">
<input class="input" type="file" name="file" required <input class="input" type="file" name="file" id="upload-file-input" required
accept="image/*,video/*,application/pdf"> accept="image/*,video/*,application/pdf">
</div> </div>
</div> </div>
@ -706,10 +718,14 @@ document.addEventListener('keydown', function(e) {
<div class="column is-narrow"> <div class="column is-narrow">
<div class="field"> <div class="field">
<label class="label">&nbsp;</label> <label class="label">&nbsp;</label>
<button class="button is-primary" type="submit">Hochladen</button> <button class="button is-primary" type="button" id="upload-btn" onclick="startUpload()">Hochladen</button>
</div> </div>
</div> </div>
</div> </div>
<div id="upload-progress-wrap" style="display:none" class="mt-2">
<progress id="upload-progress" class="progress is-primary" value="0" max="100">0%</progress>
</div>
<div id="upload-error" class="notification is-danger is-light mt-2" style="display:none"></div>
</form> </form>
</div> </div>
@ -800,6 +816,62 @@ if (sortableEl) {
} }
}); });
} }
// XHR-Upload mit Fortschrittsbalken
function startUpload() {
var form = document.getElementById('upload-form');
var fileInput = document.getElementById('upload-file-input');
var btn = document.getElementById('upload-btn');
var progressWrap = document.getElementById('upload-progress-wrap');
var progress = document.getElementById('upload-progress');
var errorBox = document.getElementById('upload-error');
errorBox.style.display = 'none';
if (!fileInput.files || fileInput.files.length === 0) {
errorBox.textContent = 'Bitte zuerst eine Datei auswählen.';
errorBox.style.display = '';
return;
}
var formData = new FormData(form);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
progress.value = pct;
progress.textContent = pct + '%';
}
};
xhr.onloadstart = function() {
btn.style.display = 'none';
progressWrap.style.display = '';
progress.value = 0;
};
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 400) {
window.location.href = '/manage/{{.Screen.Slug}}?msg=uploaded';
} else {
progressWrap.style.display = 'none';
btn.style.display = '';
errorBox.textContent = 'Upload fehlgeschlagen (HTTP ' + xhr.status + '): ' + xhr.responseText;
errorBox.style.display = '';
}
};
xhr.onerror = function() {
progressWrap.style.display = 'none';
btn.style.display = '';
errorBox.textContent = 'Netzwerkfehler beim Upload. Bitte erneut versuchen.';
errorBox.style.display = '';
};
xhr.open('POST', form.action);
xhr.send(formData);
}
</script> </script>
</body> </body>

View file

@ -411,7 +411,7 @@ func HandleUpdateItemUI(playlists *store.PlaylistStore) http.HandlerFunc {
if d, err := strconv.Atoi(strings.TrimSpace(r.FormValue("duration_seconds"))); err == nil && d > 0 { if d, err := strconv.Atoi(strings.TrimSpace(r.FormValue("duration_seconds"))); err == nil && d > 0 {
durationSeconds = d durationSeconds = d
} }
enabled := r.FormValue("enabled") != "false" enabled := r.FormValue("enabled") == "true"
validFrom, _ := parseOptionalTime(r.FormValue("valid_from")) validFrom, _ := parseOptionalTime(r.FormValue("valid_from"))
validUntil, _ := parseOptionalTime(r.FormValue("valid_until")) validUntil, _ := parseOptionalTime(r.FormValue("valid_until"))