diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index 8f6f06b..4110a05 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -1663,6 +1663,32 @@ def api_cloudinit_templates_get(tid): return jsonify({"error": "not found"}), 404 +@app.route("/api/cloudinit-templates/", methods=["PUT"]) +@require_admin +def api_cloudinit_templates_update(tid): + """Update an existing template (name, user_data, meta_data, network_config). Id unchanged.""" + body = request.get_json(force=True, silent=True) or {} + data = _load_cloudinit_templates() + templates = data.get("templates", []) + for i, t in enumerate(templates): + if t.get("id") == tid: + name = (body.get("name") or t.get("name") or "").strip() + if not name: + return jsonify({"ok": False, "error": "name required"}), 400 + templates[i] = { + "id": tid, + "name": name, + "user_data": body.get("user_data", t.get("user_data", "")), + "meta_data": body.get("meta_data", t.get("meta_data", "")), + "network_config": body.get("network_config", t.get("network_config", "")), + } + data["templates"] = templates + if not _save_cloudinit_templates(data): + return jsonify({"ok": False, "error": "Failed to save"}), 500 + return jsonify({"ok": True, "id": tid, "name": name}) + return jsonify({"ok": False, "error": "not found"}), 404 + + @app.route("/api/cloudinit-templates/", methods=["DELETE"]) @require_admin def api_cloudinit_templates_delete(tid): diff --git a/emmc-provisioning/dashboard/templates/cloudinit_build.html b/emmc-provisioning/dashboard/templates/cloudinit_build.html index dd44c35..767e48f 100644 --- a/emmc-provisioning/dashboard/templates/cloudinit_build.html +++ b/emmc-provisioning/dashboard/templates/cloudinit_build.html @@ -99,6 +99,7 @@ Templates: + @@ -159,7 +160,7 @@ list.forEach(function(t) { var o = document.createElement('option'); o.value = t.id; o.textContent = t.name; sel.appendChild(o); }); var ul = document.getElementById('buildTemplateList'); ul.innerHTML = list.map(function(t) { - return '
  • ' + escapeHtml(t.name) + '
  • '; + return '
  • ' + escapeHtml(t.name) + '
  • '; }).join('') || '
  • No templates
  • '; ul.querySelectorAll('.load-tpl').forEach(function(b) { b.onclick = function() { @@ -170,6 +171,9 @@ }); }; }); + ul.querySelectorAll('.upd-tpl').forEach(function(b) { + b.onclick = function() { updateTemplate(b.getAttribute('data-id')); }; + }); ul.querySelectorAll('.del-tpl').forEach(function(b) { b.onclick = function() { if (!confirm('Delete template?')) return; @@ -178,6 +182,12 @@ }); }).catch(function() {}); } + function updateTemplate(tid) { + var body = { user_data: document.getElementById('buildUserData').value, meta_data: document.getElementById('buildMetaData').value, network_config: document.getElementById('buildNetworkConfig').value }; + authFetch('/api/cloudinit-templates/' + encodeURIComponent(tid), { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r) { return r.json(); }).then(function(d) { + if (d.ok) { fetchTemplates(); alert('Template updated'); } else alert(d.error || 'Update failed'); + }).catch(function() { alert('Update failed'); }); + } function loadTemplateFromSelect() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) authFetch('/api/cloudinit-templates/' + s.value).then(function(r) { return r.json(); }).then(function(t) { @@ -200,6 +210,11 @@ }); }; document.getElementById('buildTemplateLoad').onclick = loadTemplateFromSelect; + document.getElementById('buildTemplateUpdate').onclick = function() { + var s = document.getElementById('buildTemplateSelect'); + if (!s || !s.value) { alert('Select a template to update'); return; } + updateTemplate(s.value); + }; document.getElementById('buildTemplateSave').onclick = saveTemplate; fetchBuildStatus(); diff --git a/emmc-provisioning/dashboard/templates/index.html b/emmc-provisioning/dashboard/templates/index.html index 7106ef6..569fd6c 100644 --- a/emmc-provisioning/dashboard/templates/index.html +++ b/emmc-provisioning/dashboard/templates/index.html @@ -449,6 +449,7 @@

    Templates: +

      @@ -824,11 +825,14 @@ } if (listEl) { listEl.innerHTML = list.map(function(t) { - return '
    • ' + escapeHtml(t.name) + '
    • '; + return '
    • ' + escapeHtml(t.name) + '
    • '; }).join('') || '
    • No templates saved.
    • '; listEl.querySelectorAll('.template-load-btn').forEach(function(btn) { btn.onclick = function() { loadTemplate(btn.getAttribute('data-id')); }; }); + listEl.querySelectorAll('.template-upd-btn').forEach(function(btn) { + btn.onclick = function() { updateTemplate(btn.getAttribute('data-id')); }; + }); listEl.querySelectorAll('.template-del-btn').forEach(function(btn) { btn.onclick = function() { if (!confirm('Delete this template?')) return; @@ -849,6 +853,19 @@ if (nc) nc.value = t.network_config || ''; }).catch(function() {}); } + function updateTemplate(id) { + var ud = document.getElementById('buildUserData'); + var md = document.getElementById('buildMetaData'); + var nc = document.getElementById('buildNetworkConfig'); + fetch('/api/cloudinit-templates/' + encodeURIComponent(id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ + user_data: ud ? ud.value : '', + meta_data: md ? md.value : '', + network_config: nc ? nc.value : '' + })}).then(function(r) { return r.json(); }).then(function(d) { + if (d.ok) { fetchCloudInitTemplates(); alert('Template updated'); } + else alert(d.error || 'Update failed'); + }).catch(function() { alert('Update failed'); }); + } function saveTemplate() { var name = prompt('Template name'); if (!name || !name.trim()) return; @@ -972,6 +989,8 @@ if (variantSel) variantSel.onchange = fetchRaspiosUrl; var templateLoadBtn = document.getElementById('buildTemplateLoad'); if (templateLoadBtn) templateLoadBtn.onclick = function() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) loadTemplate(s.value); }; + var templateUpdateBtn = document.getElementById('buildTemplateUpdate'); + if (templateUpdateBtn) templateUpdateBtn.onclick = function() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) updateTemplate(s.value); else alert('Select a template to update'); }; var templateSaveBtn = document.getElementById('buildTemplateSave'); if (templateSaveBtn) templateSaveBtn.onclick = saveTemplate; setInterval(fetchStatus, 2000);