diff --git a/admin.html b/admin.html index 75cd1da..b75028c 100644 --- a/admin.html +++ b/admin.html @@ -12,7 +12,7 @@ - +
@@ -147,13 +147,15 @@
Dienste & Kategorien (Konfiguration)
-
+
- + +
-
+ +
@@ -242,6 +244,6 @@
- + diff --git a/admin.js b/admin.js index cd7fdcb..70705b9 100644 --- a/admin.js +++ b/admin.js @@ -17,7 +17,9 @@ const loginMessage = document.querySelector('#loginMessage'); const adminUser = document.querySelector('#adminUser'); const serviceEditor = document.querySelector('#serviceEditor'); const checkAllButton = document.querySelector('#checkAllButton'); -const saveLocalButton = document.querySelector('#saveLocalButton'); +const addServiceButton = document.querySelector('#addServiceButton'); +const saveServerButton = document.querySelector('#saveServerButton'); +const editorFeedback = document.querySelector('#editorFeedback'); const resetLocalButton = document.querySelector('#resetLocalButton'); const downloadConfigButton = document.querySelector('#downloadConfigButton'); const refreshOpsButton = document.querySelector('#refreshOpsButton'); @@ -193,41 +195,128 @@ function categoryOptions(selected) { )).join(''); } +function iconOptions(selected) { + const icons = ['shield', 'folder', 'lock', 'film', 'headphones', 'book', 'git', 'chess', 'user']; + return icons.map((icon) => ( + `` + )).join(''); +} + function renderEditor() { serviceEditor.innerHTML = config.services.map((service, index) => `
-
- ${service.name} - ${service.domain} + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + +
+ +
+ +
- - - -
Noch nicht geprueft.
`).join(''); - serviceEditor.querySelectorAll('select,input').forEach((input) => { - input.addEventListener('change', () => { - const row = input.closest('.admin-row'); - const service = config.services[Number(row.dataset.index)]; - service[input.dataset.field] = input.value; + // Wire up two-way sync for color pickers and text inputs, and event listeners + serviceEditor.querySelectorAll('.admin-row').forEach((row) => { + const index = Number(row.dataset.index); + const service = config.services[index]; + + // Wire standard input changes + row.querySelectorAll('select, input:not(.accent-color-picker)').forEach((input) => { + input.addEventListener('input', () => { + service[input.dataset.field] = input.value; + }); + input.addEventListener('change', () => { + service[input.dataset.field] = input.value; + }); }); + + // Wire color picker sync + const textInput = row.querySelector('input[data-field="accent"]'); + const colorPicker = row.querySelector('.accent-color-picker'); + + if (textInput && colorPicker) { + colorPicker.addEventListener('input', () => { + textInput.value = colorPicker.value; + service.accent = colorPicker.value; + }); + textInput.addEventListener('input', () => { + const val = textInput.value; + if (/^#[0-9A-Fa-f]{6}$/.test(val)) { + colorPicker.value = val; + } + service.accent = val; + }); + } }); serviceEditor.querySelectorAll('.check-one').forEach((button) => { button.addEventListener('click', () => checkRow(button.closest('.admin-row'))); }); + + serviceEditor.querySelectorAll('.delete-one').forEach((button) => { + button.addEventListener('click', () => { + const row = button.closest('.admin-row'); + const index = Number(row.dataset.index); + const service = config.services[index]; + const confirmed = window.confirm(`Dienst "${service.name || service.id || 'unbekannt'}" wirklich loeschen?`); + if (!confirmed) return; + config.services.splice(index, 1); + renderEditor(); + }); + }); } async function probe(url) { @@ -282,8 +371,102 @@ async function checkRow(row) { diagnostic.textContent = `${label} - ${result.ms} ms - ${result.detail}`; } -function saveLocal() { - localStorage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(config, null, 2)); +function addService() { + const newService = { + id: `dienst-${Date.now()}`, + name: 'Neuer Dienst', + description: 'Beschreibung des Dienstes', + domain: 'neu.mischlabs.de', + url: 'https://neu.mischlabs.de', + category: config.categories[0]?.id || 'access', + accent: '#38bdf8', + icon: 'shield', + keywords: 'neu dienst' + }; + config.services.push(newService); + renderEditor(); + + // Scroll new row into view smoothly + const rows = serviceEditor.querySelectorAll('.admin-row'); + const lastRow = rows[rows.length - 1]; + if (lastRow) { + lastRow.scrollIntoView({ behavior: 'smooth', block: 'center' }); + lastRow.classList.add('is-new'); + } +} + +function showFeedback(type, message) { + if (!editorFeedback) return; + editorFeedback.style.display = 'block'; + editorFeedback.textContent = message; + + if (type === 'success') { + editorFeedback.style.background = 'rgba(52, 211, 153, 0.12)'; + editorFeedback.style.borderColor = 'rgba(52, 211, 153, 0.35)'; + editorFeedback.style.color = '#34d399'; + } else { + editorFeedback.style.background = 'rgba(251, 113, 133, 0.12)'; + editorFeedback.style.borderColor = 'rgba(251, 113, 133, 0.35)'; + editorFeedback.style.color = '#fb7185'; + } + + if (type === 'success') { + setTimeout(() => { + editorFeedback.style.display = 'none'; + }, 5000); + } +} + +async function saveServer() { + if (!tokenSet?.id_token) { + showFeedback('error', 'Nicht angemeldet oder Sitzung abgelaufen.'); + return; + } + + if (!config || !Array.isArray(config.services) || !Array.isArray(config.categories)) { + showFeedback('error', 'Ungueltiges Konfigurationsformat.'); + return; + } + + for (const service of config.services) { + if (!service.id || !service.name || !service.domain || !service.url || !service.category) { + showFeedback('error', 'Bitte füllen Sie ID, Name, Domain, URL und Kategorie für alle Dienste aus.'); + return; + } + } + + const originalText = saveServerButton.textContent; + saveServerButton.disabled = true; + saveServerButton.textContent = 'Speichere...'; + + try { + const response = await fetch('/api/services/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${tokenSet.id_token}` + }, + body: JSON.stringify(config) + }); + + const data = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`); + } + + showFeedback('success', 'Konfiguration erfolgreich auf dem Server gespeichert!'); + localStorage.removeItem(LOCAL_CONFIG_KEY); + + window.setTimeout(() => { + refreshOps(); + }, 500); + + } catch (error) { + showFeedback('error', `Speichern fehlgeschlagen: ${error.message}`); + } finally { + saveServerButton.disabled = false; + saveServerButton.textContent = originalText; + } } function downloadConfig() { @@ -1278,10 +1461,13 @@ checkAllButton.addEventListener('click', () => { [...serviceEditor.querySelectorAll('.admin-row')].forEach((row) => checkRow(row)); }); -saveLocalButton.addEventListener('click', () => { - saveLocal(); - window.alert('Lokal gespeichert. Das Dashboard in diesem Browser nutzt diese Aenderungen.'); -}); +if (addServiceButton) { + addServiceButton.addEventListener('click', addService); +} + +if (saveServerButton) { + saveServerButton.addEventListener('click', saveServer); +} resetLocalButton.addEventListener('click', () => { localStorage.removeItem(LOCAL_CONFIG_KEY); diff --git a/server.js b/server.js index 86f2934..8c8ef2e 100644 --- a/server.js +++ b/server.js @@ -1218,6 +1218,43 @@ const server = http.createServer(async (req, res) => { return; } + if (req.url?.startsWith('/api/services/save')) { + if (req.method !== 'POST') { + json(res, 405, { error: 'Method not allowed' }); + return; + } + + try { + await verifyAdminRequest(req); + + const body = await new Promise((resolve, reject) => { + let data = ''; + req.on('data', chunk => data += chunk); + req.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + reject(new Error('Invalid JSON')); + } + }); + req.on('error', reject); + }); + + if (!body || !Array.isArray(body.categories) || !Array.isArray(body.services)) { + json(res, 400, { error: 'Invalid configuration format' }); + return; + } + + const servicesPath = path.join(PUBLIC_DIR, 'services.json'); + await fsp.writeFile(servicesPath, JSON.stringify(body, null, 2), 'utf-8'); + + json(res, 200, { success: true, message: 'Configuration saved successfully' }); + } catch (error) { + json(res, error.statusCode || 500, { error: error.message }); + } + return; + } + if (req.url?.startsWith('/api/disks/scan-folder')) { if (req.method !== 'GET') { json(res, 405, { error: 'Method not allowed' }); diff --git a/style.css b/style.css index 9496a7f..db42370 100644 --- a/style.css +++ b/style.css @@ -1016,14 +1016,15 @@ main { } .admin-row { - display: grid; - grid-template-columns: minmax(140px, 1fr) minmax(120px, 0.6fr) minmax(180px, 1.1fr) minmax(130px, 0.9fr) auto; - gap: 10px; - align-items: end; - padding: 12px; - background: rgba(7, 9, 20, 0.42); + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + background: rgba(17, 22, 36, 0.42); border: 1px solid var(--border); border-radius: var(--radius); + margin-bottom: 16px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; } .admin-row > div:first-child { @@ -1945,3 +1946,42 @@ main { background: rgba(255, 255, 255, 0.18) !important; transform: scale(1.15); } + +/* Service Editor Advanced Styling */ +.admin-form-group-row { + display: flex; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.admin-form-group-row > label { + flex: 1 1 200px; + display: grid; + gap: 6px; +} + +.admin-row-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: flex-end; + margin-top: 4px; + border-top: 1px solid var(--border); + padding-top: 12px; +} + +.admin-row.is-new { + animation: new-row-pulse 2s ease; +} + +@keyframes new-row-pulse { + 0% { + border-color: #38bdf8; + box-shadow: 0 0 12px rgba(56, 189, 248, 0.4); + } + 100% { + border-color: var(--border); + box-shadow: none; + } +}