From 65e60899f99c4c874cffbf07b0121e9265977aec Mon Sep 17 00:00:00 2001 From: Deploymentcenter Bot Date: Sat, 8 Aug 2026 10:39:35 +0200 Subject: [PATCH] fix(webui): Ansicht nach dem Speichern beibehalten, Lizenzmodul zusammengelegt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rücksprung auf das Dashboard Nach jedem Speichern landete man auf der Standard-Unteransicht des Moduls statt dort, wo man gearbeitet hatte. Drei Ursachen: - Die Handler setzten nur den Modul-Anker (#tab-watchdog). Damit blieb die Unteransicht unbestimmt und es wurde die erste angezeigt. - Die Wiederherstellung aus dem Anker leitete den Modulnamen aus dem Namen der Unteransicht ab (sub--). Für "sub-update-releases" ergab das "update" statt "updateservice". - Der zugehörige Navigationsknopf wurde über seinen Beschriftungstext gesucht, der den Modulnamen nie enthält. Der Rückweg schlug also immer fehl. Jetzt teilt das Formular beim Absenden mit, welche Unteransicht offen war (_return), und der Server leitet dorthin zurück. Umgesetzt als zentraler submit-Zuhörer, damit künftige Formulare das Verhalten erben. Die Zuordnung Unteransicht -> Modul kommt aus derselben Liste, die auch die Navigation aufbaut, und die Knöpfe tragen eine eindeutige Kennung statt Text. Zusätzlich wird die Ansicht jetzt auch bei Vor- und Zurück-Navigation im Browser wiederhergestellt. Lizenzmodul Vier Unteransichten für eine Aufgabe sind auf zwei sinnvolle reduziert: - "Dashboard" zeigte drei Zahlen; sie stehen jetzt über der Liste. - "Lizenz Bearbeiten & HW" ist entfallen. Zum Bearbeiten musste man die gerade angeklickte Lizenz in einer Auswahlliste erneut suchen. Jetzt öffnet sich ein Dialog direkt über der Tabelle, die Hardware-Liste steht daneben. - "Settings" enthielt zwei fest verdrahtete URLs und ist in "Integration" aufgegangen, zusammen mit einem Beispielaufruf. - Der dort angezeigte "Ed25519 Public Key" war frei erfunden und wurde entfernt; die Offline-Vorschau nennt jetzt das tatsächlich verwendete HMAC-SHA256 und weist darauf hin, dass nur der Download signiert ist. Nebenbei Der Inhalt des Projekte-Moduls lag direkt im Modul-Container, ohne subtab-content. Der Navigationseintrag "Projekte Übersicht" zeigte damit ins Leere. Jetzt einheitlich wie alle anderen Module. Co-Authored-By: Claude Opus 5 --- public/index.php | 656 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 459 insertions(+), 197 deletions(-) diff --git a/public/index.php b/public/index.php index 7f5ca39..9f6b481 100644 --- a/public/index.php +++ b/public/index.php @@ -226,9 +226,25 @@ function dc_flash(string $message, string $type = 'success'): void /** * Post/Redirect/Get: leitet nach einer Aktion auf dieselbe Seite um, damit * ein Neuladen das Formular nicht erneut abschickt. + * + * Das Formular teilt beim Absenden im Feld _return mit, welche Unteransicht + * gerade geoeffnet war; dorthin wird zurueckgeleitet. Der als $fragment + * uebergebene Wert dient nur noch als Rueckfallebene, etwa wenn JavaScript + * abgeschaltet ist. + * + * Zuvor wurde ausschliesslich der Modul-Anker gesetzt. Man legte einen Monitor + * an und landete anschliessend auf dem Watchdog-Dashboard statt im Formular. */ function dc_redirect(string $fragment = ''): void { + $return = $_POST['_return'] ?? null; + + // Nur eigene Ansichtskennungen zulassen - der Wert landet in einem + // Location-Header. + if (is_string($return) && preg_match('/^(?:tab|sub)-[a-z0-9-]{1,60}$/', $return) === 1) { + $fragment = '#' . $return; + } + $query = $_SERVER['QUERY_STRING'] ?? ''; $target = '/index.php' . ($query !== '' ? '?' . $query : '') . $fragment; @@ -1722,6 +1738,10 @@ $csrfField = Csrf::field();
+ +

📁 Neues Projekt anlegen / Bearbeiten

@@ -1789,6 +1809,7 @@ $csrfField = Csrf::field();
+
@@ -1951,48 +1972,60 @@ $csrfField = Csrf::field(); +
- -
-
+
+ $l['status'] === 'active')); + $blockedActivations = count(array_filter($activations, static fn($a): bool => !empty($a['is_blocked']))); + ?> +
-
Registrierte Projekte
-
+
Lizenzen gesamt
+
-
Gültige Lizenzen
-
$l['status'] === 'active')) ?>
+
davon aktiv
+
Aktivierte Systeme
+
+
Gesperrte Hardware
+
+
-
- -
-

Neuen Lizenzschlüssel generieren

+

🔑 Neuen Lizenzschlüssel generieren

- +
- +
- +
@@ -2004,7 +2037,7 @@ $csrfField = Csrf::field();
- +
@@ -2023,81 +2056,77 @@ $csrfField = Csrf::field(); - + - - - - / - - - - 📥 .lic - + + Noch keine Lizenzen angelegt. + + + + + + 0 && $used >= $max; + $licJson = e((string)json_encode($l, JSON_INVALID_UTF8_SUBSTITUTE)); + ?> + + + + + + +
+ + + + + / + + + + + + + + + + + +
+ + 📥 .lic +
+ + + + + +
+
-
- - -
-
-

✏️ Lizenz Vollständig Bearbeiten

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-

Aktivierte Hardware-IDs verwalten

+
+

🖥️ Aktivierte Hardware

+
- + @@ -2105,44 +2134,57 @@ $csrfField = Csrf::field(); + + + + + + - - + + - - - + + @@ -2154,29 +2196,35 @@ $csrfField = Csrf::field();
-

💾 Signierte Offline-Lizenzdatei erzeugen (.lic)

-

- Erzeugt eine signierte .lic Offline-Lizenzdatei für Air-Gapped Kundensysteme. +

💾 Offline-Lizenzdatei (.lic)

+

+ Für Systeme ohne Internetzugang. Die Datei wird serverseitig erzeugt und mit + HMAC-SHA256 über den Schlüssel aus security.license_key signiert. + Am schnellsten geht es über den 📥 .lic-Knopf direkt in der Lizenzliste.

- +
- +
- -
Wählen Sie oben eine Lizenz aus und klicken Sie auf 'Offline Payload Generieren'.
+ +
Lizenz auswählen und auf „Vorschau erzeugen“ klicken.
+

+ Diese Vorschau dient nur der Ansicht. Die gültige, signierte Datei liefert + der 📥 .lic-Knopf in der Lizenzliste. +

@@ -2196,13 +2244,16 @@ $csrfField = Csrf::field(); + + + - - - - - + + + + + @@ -2210,37 +2261,51 @@ $csrfField = Csrf::field(); - +
-

🔑 Server Ed25519 Public Key & Endpunkte

+

💻 Endpunkte & Anbindung

- - + +
- - + +
-
-
- -
-
-

⚙️ Moduleinstellungen & API Konfiguration

-
-
- - -
-
- - -
+
+ Die Antwort von /validate trägt den Lizenzzustand im Feld + status auf oberster Ebene (valid, revoked, + expired, not_found, activation_limit) — + bewusst ohne den Umschlag der übrigen API, weil bereits ausgelieferte Clients + dieses Format erwarten.
+ +
POST /api/license/v1/validate +Content-Type: application/json + +{ + "product": "myapp", + "license_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", + "hardware_id": "2:win:a765bd47...", + "hostname": "DESKTOP-01", + "app_version": "v1.4.2", + "nonce": "zufallswert" +} + +Antwort: +{ "type": "validation_result", "status": "valid", + "expires_at": 1786435199, "cache_ttl_hours": 168, + "message": "License is valid" }
+ +

+ Fertige Anbindung für .NET: Deploymentcenter.Client + (LicenseClient, HardwareId, verschlüsselter Offline-Cache). + Ausführlich in docs/LICENSE_INTEGRATION_GUIDE.md. +

@@ -3785,13 +3850,13 @@ SYSTEM { id: 'sub-projects-list', label: '📊 Projekte Übersicht', active: true } ], 'license': [ - { id: 'sub-license-dashboard', label: '📊 Dashboard', active: true }, - { id: 'sub-license-licenses', label: '🔑 Lizenzverwaltung' }, - { id: 'sub-license-details', label: '🔍 Lizenz Bearbeiten & HW' }, + // Dashboard, "Lizenz Bearbeiten & HW" und Settings sind entfallen: + // vier Unteransichten fuer eine Aufgabe. Bearbeitet wird jetzt + // ueber einen Dialog direkt in der Liste. + { id: 'sub-license-licenses', label: '🔑 Lizenzen & Hardware', active: true }, { id: 'sub-license-offline', label: '💾 Offline-Lizenzen (.lic)' }, { id: 'sub-license-audit', label: '📜 Audit-Log' }, - { id: 'sub-license-integration', label: '💻 Integration' }, - { id: 'sub-license-settings', label: '⚙️ Settings' } + { id: 'sub-license-integration', label: '💻 Integration' } ], 'watchdog': [ { id: 'sub-watchdog-dashboard', label: '📊 Dashboard', active: true }, @@ -3822,21 +3887,80 @@ SYSTEM ] }; + /** + * Ermittelt, zu welchem Modul eine Unteransicht gehoert. + * + * Zuvor wurde das aus dem Namen geraten (sub--), was bei + * "sub-update-releases" fehlschlug - das Modul heisst updateservice. + * Die Zuordnung kommt jetzt aus derselben Liste, die auch die + * Navigation aufbaut, und ist damit immer korrekt. + */ + function moduleOfSubtab(subtabId) { + for (const moduleName of Object.keys(subnavItems)) { + if (subnavItems[moduleName].some(item => item.id === subtabId)) { + return moduleName; + } + } + return null; + } + // Render Subnav links in Top-Bar - function renderTopBarSubnav(moduleName) { + function renderTopBarSubnav(moduleName, activeSubtabId) { const container = document.getElementById('topbarSubnav'); container.innerHTML = ''; const items = subnavItems[moduleName] || []; items.forEach(item => { + const isActive = activeSubtabId ? (item.id === activeSubtabId) : !!item.active; + const btn = document.createElement('button'); - btn.className = `subnav-link ${item.active ? 'active' : ''}`; + btn.className = `subnav-link ${isActive ? 'active' : ''}`; btn.innerText = item.label; + // Eindeutige Kennung statt Textvergleich - der Beschriftungstext + // taugt nicht zum Wiederfinden. + btn.dataset.subtab = item.id; btn.onclick = () => switchSubTab(moduleName, item.id, btn); container.appendChild(btn); }); } + /** + * Merkt sich beim Absenden, welche Unteransicht offen war. + * + * Der Server haengt diesen Wert nach dem Speichern wieder an die URL. + * Ohne das landete man stets auf dem Dashboard des Moduls - man legte + * einen Monitor an und wurde aus dem Formular geworfen. + * + * Als zentraler Zuhoerer umgesetzt, damit auch kuenftige Formulare das + * Verhalten automatisch erben. + */ + document.addEventListener('submit', (event) => { + const form = event.target; + if (!(form instanceof HTMLFormElement)) { + return; + } + if ((form.method || '').toLowerCase() !== 'post') { + return; + } + if (form.querySelector('input[name="_return"]')) { + return; + } + + const activeSub = document.querySelector('.subtab-content.active'); + const activeTab = document.querySelector('.main-content > .tab-content.active'); + const target = activeSub ? activeSub.id : (activeTab ? activeTab.id : ''); + + if (!target) { + return; + } + + const field = document.createElement('input'); + field.type = 'hidden'; + field.name = '_return'; + field.value = target; + form.appendChild(field); + }, true); + // Sidebar Toggle Handler function toggleSidebar() { const sidebar = document.getElementById('sidebar'); @@ -3869,7 +3993,11 @@ SYSTEM } // Main Module Navigation Switcher - function switchMainTab(moduleName, el) { + // + // activeSubtabId und updateHash werden beim Wiederherstellen aus der URL + // gesetzt: dort soll eine bestimmte Unteransicht geoeffnet und der + // Anker nicht ueberschrieben werden. + function switchMainTab(moduleName, el, activeSubtabId, updateHash = true) { document.querySelectorAll('.main-content > .tab-content').forEach(t => t.classList.remove('active')); document.querySelectorAll('.nav-link').forEach(n => n.classList.remove('active')); @@ -3877,8 +4005,10 @@ SYSTEM if (targetTab) { targetTab.classList.add('active'); } - if (el) { - el.classList.add('active'); + + const navLink = el || document.querySelector(`.nav-link[onclick*="'${moduleName}'"]`); + if (navLink) { + navLink.classList.add('active'); } const pageTitles = { @@ -3892,8 +4022,25 @@ SYSTEM 'system': 'System & Datenbank Status' }; document.getElementById('topPageTitle').innerText = pageTitles[moduleName] || 'Deploymentcenter'; - renderTopBarSubnav(moduleName); - location.hash = 'tab-' + moduleName; + + renderTopBarSubnav(moduleName, activeSubtabId); + + // Unteransicht passend zur Navigation einblenden + const items = subnavItems[moduleName] || []; + const wanted = activeSubtabId + || (items.find(item => item.active) || items[0] || {}).id; + + if (targetTab && wanted) { + targetTab.querySelectorAll('.subtab-content').forEach(s => s.classList.remove('active')); + const sub = document.getElementById(wanted); + if (sub) { + sub.classList.add('active'); + } + } + + if (updateHash) { + location.hash = wanted || ('tab-' + moduleName); + } } // Die frueher hier stehende Funktion filterBugtrackerTable() ist entfallen. @@ -4115,9 +4262,15 @@ SYSTEM if (targetSub) { targetSub.classList.add('active'); } - if (el) { - el.classList.add('active'); + + // Ohne uebergebenen Knopf ueber die Kennung finden, damit die + // Markierung auch beim Wiederherstellen aus der URL stimmt. + const button = el + || document.querySelector(`#topbarSubnav .subnav-link[data-subtab="${subtabId}"]`); + if (button) { + button.classList.add('active'); } + location.hash = subtabId; } @@ -4210,25 +4363,47 @@ SYSTEM } } - // License Editing Functions - function openEditLicense(l) { - switchMainTab('license', document.querySelector('.nav-link[onclick*="license"]')); - const subLink = document.querySelector('#topbarSubnav .subnav-link:nth-child(3)'); - switchSubTab('license', 'sub-license-details', subLink); + // ------------------------------------------------------------------ + // Lizenz bearbeiten + // + // Frueher sprang der Edit-Knopf in eine eigene Unteransicht mit einer + // Auswahlliste, in der man die gerade angeklickte Lizenz erneut suchen + // musste. Jetzt oeffnet sich ein Dialog direkt ueber der Tabelle. + // ------------------------------------------------------------------ + function openEditLicense(license) { + const setVal = (id, value) => { + const el = document.getElementById(id); + if (el) el.value = value ?? ''; + }; - const select = document.getElementById('licEditSelect'); - select.value = l.id; - loadLicenseData(select); - window.scrollTo({ top: 0, behavior: 'smooth' }); + setVal('licEditId', license.id); + setVal('licEditCustomerName', license.customer_name); + setVal('licEditCustomerEmail', license.customer_email); + setVal('licEditStatus', license.status || 'active'); + setVal('licEditMaxAct', license.max_activations || 2); + setVal('licEditNotes', license.notes); + + // Das Datumsfeld erwartet YYYY-MM-DD, die Datenbank liefert einen + // vollstaendigen Zeitstempel. + const expires = license.expires_at ? String(license.expires_at).substring(0, 10) : ''; + setVal('licEditExpiresAt', expires); + + const title = document.getElementById('licEditTitle'); + if (title) { + title.textContent = `${license.license_key} — ${license.customer_name || 'ohne Kundenangabe'}`; + } + + const activationInfo = document.getElementById('licEditActivations'); + if (activationInfo) { + activationInfo.textContent = + `${license.active_count ?? 0} von ${license.max_activations ?? 0} Aktivierungen belegt`; + } + + document.getElementById('licEditModal').style.display = 'flex'; } - function loadLicenseData(selectEl) { - const opt = selectEl.options[selectEl.selectedIndex]; - document.getElementById('editCustomerName').value = opt.getAttribute('data-cname') || ''; - document.getElementById('editCustomerEmail').value = opt.getAttribute('data-cemail') || ''; - document.getElementById('editStatus').value = opt.getAttribute('data-status') || 'active'; - document.getElementById('editMaxAct').value = opt.getAttribute('data-max') || '2'; - document.getElementById('editExpiresAt').value = opt.getAttribute('data-exp') || ''; + function closeEditLicense() { + document.getElementById('licEditModal').style.display = 'none'; } // Token Masking Toggle & Copy @@ -4257,47 +4432,70 @@ SYSTEM } } + // Reine Ansichtsvorschau. Die vorherige Fassung erzeugte hier eine + // Zeichenkette, die sie "ED25519_SIG_" nannte - tatsaechlich war es ein + // base64-kodierter Schluessel ohne jede Signaturwirkung. Die gueltige, + // serverseitig signierte Datei liefert der .lic-Knopf in der Liste. function generateOfflinePayload() { const key = document.getElementById('offlineLicSelect').value; - const hwId = document.getElementById('offlineHwId').value || 'HWID-GENERAL'; - - const payload = { - "type": "offline_license_file", - "issued_at": Math.floor(Date.now() / 1000), - "license_key": key, - "hardware_id": hwId, - "status": "valid", - "valid_until": "2027-12-31 23:59:59", - "signature": "ED25519_SIG_" + btoa(key + hwId).substring(0, 32) + const hwId = document.getElementById('offlineHwId').value || '(beliebig)'; + + const preview = { + type: 'offline_license_file', + issued_at: Math.floor(Date.now() / 1000), + license_key: key, + hardware_id: hwId, + signature_alg: 'HMAC-SHA256', + signature: '(wird beim Herunterladen serverseitig erzeugt)' }; - document.getElementById('offlineResultBox').innerText = JSON.stringify(payload, null, 2); + document.getElementById('offlineResultBox').innerText = JSON.stringify(preview, null, 2); } // Initialize default subnav for Overview renderTopBarSubnav('overview'); - // Restore tab based on URL hash - window.addEventListener('load', () => { + /** + * Stellt Modul und Unteransicht aus dem Anker der URL wieder her. + * + * Die vorherige Fassung leitete den Modulnamen aus dem Anker ab + * (sub--) und suchte den Navigationsknopf anhand seines + * Beschriftungstextes. Beides ging schief: "sub-update-releases" gehoert + * zum Modul updateservice, und keine Beschriftung enthaelt den + * Modulnamen. Der Rueckweg landete deshalb immer auf dem Dashboard. + */ + function restoreViewFromHash() { const hash = location.hash.replace('#', ''); - if (hash) { - if (hash.startsWith('tab-')) { - const module = hash.replace('tab-', ''); - const link = document.querySelector(`.nav-link[onclick*="${module}"]`); - if (link) switchMainTab(module, link); - } else if (hash.startsWith('sub-')) { - const moduleParts = hash.split('-'); - const module = moduleParts[1]; - const mainLink = document.querySelector(`.nav-link[onclick*="${module}"]`); - if (mainLink) switchMainTab(module, mainLink); - - setTimeout(() => { - const subLink = Array.from(document.querySelectorAll('#topbarSubnav .subnav-link')).find(l => l.innerText.toLowerCase().includes(module)); - if (subLink) switchSubTab(module, hash, subLink); - }, 50); - } + if (!hash) { + return; } - }); + + if (hash.startsWith('tab-')) { + const moduleName = hash.substring(4); + if (subnavItems[moduleName]) { + switchMainTab(moduleName, null, null, false); + } + return; + } + + if (!hash.startsWith('sub-')) { + return; + } + + const moduleName = moduleOfSubtab(hash); + if (!moduleName) { + return; + } + + // Die Unteransicht wird direkt mit uebergeben - die Navigation wird + // synchron aufgebaut, ein Timeout ist dafuer nicht noetig. + switchMainTab(moduleName, null, hash, false); + } + + window.addEventListener('load', restoreViewFromHash); + + // Vor- und Zurueck-Navigation im Browser ebenfalls beruecksichtigen + window.addEventListener('hashchange', restoreViewFromHash); @@ -4357,6 +4555,70 @@ SYSTEM + + +
Projekt Lizenzschlüssel Hardware-ID / QuellePlattform / Ver.Plattform Hostname Zuletzt gesehen Status
+ Noch keine Aktivierungen. +
- + -
Quelle: +
Quelle:
- - (v) + + · v - + + -
- - - - - -
-
- - - - -
+ + + +
+
+
+ + + + + +
+
+ + + + +
+
Noch keine Einträge.