fix(webui): Ansicht nach dem Speichern beibehalten, Lizenzmodul zusammengelegt
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-<modul>-<rest>). 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
92bb400f02
commit
65e60899f9
+447
-185
@@ -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();
|
||||
|
||||
<!-- ================= MODULE 1: PROJEKTE ================= -->
|
||||
<div id="tab-projects" class="tab-content">
|
||||
<!-- Einheitlich wie alle anderen Module: der Inhalt liegt in einer
|
||||
benannten Unteransicht. Zuvor stand er direkt im Modul, wodurch
|
||||
der Navigationseintrag "Projekte Uebersicht" ins Leere zeigte. -->
|
||||
<div id="sub-projects-list" class="subtab-content active">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">📁 Neues Projekt anlegen / Bearbeiten</h2></div>
|
||||
<form method="POST" action="index.php#tab-projects">
|
||||
@@ -1790,6 +1810,7 @@ $csrfField = Csrf::field();
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Project Safety Modal -->
|
||||
<div class="modal-backdrop" id="deleteProjectModal">
|
||||
@@ -1951,30 +1972,42 @@ $csrfField = Csrf::field();
|
||||
</form>
|
||||
|
||||
<!-- ================= MODULE 2: LIZENZEN ================= -->
|
||||
<!--
|
||||
Zusammengelegt: "Dashboard", "Lizenzverwaltung", "Lizenz Bearbeiten & HW"
|
||||
und "Settings" waren vier Unteransichten fuer eine Aufgabe. Das
|
||||
Dashboard zeigte drei Zahlen, Settings zwei fest verdrahtete URLs,
|
||||
und zum Bearbeiten sprang man in eine eigene Ansicht mit einer
|
||||
Auswahlliste, in der man die gerade angeklickte Lizenz erneut
|
||||
suchen musste. Jetzt eine Ansicht mit einem Bearbeiten-Dialog.
|
||||
-->
|
||||
<div id="tab-license" class="tab-content">
|
||||
|
||||
<!-- Subtab: License Dashboard -->
|
||||
<div id="sub-license-dashboard" class="subtab-content active">
|
||||
<div class="stats-grid">
|
||||
<div id="sub-license-licenses" class="subtab-content active">
|
||||
<?php
|
||||
$activeLicenses = count(array_filter($licenses, static fn($l): bool => $l['status'] === 'active'));
|
||||
$blockedActivations = count(array_filter($activations, static fn($a): bool => !empty($a['is_blocked'])));
|
||||
?>
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Registrierte Projekte</div>
|
||||
<div class="stat-value"><?= count($projects) ?></div>
|
||||
<div class="stat-header">Lizenzen gesamt</div>
|
||||
<div class="stat-value"><?= count($licenses) ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Gültige Lizenzen</div>
|
||||
<div class="stat-value"><?= count(array_filter($licenses, fn($l) => $l['status'] === 'active')) ?></div>
|
||||
<div class="stat-header">davon aktiv</div>
|
||||
<div class="stat-value" style="color:var(--success);"><?= $activeLicenses ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Aktivierte Systeme</div>
|
||||
<div class="stat-value"><?= count($activations) ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">Gesperrte Hardware</div>
|
||||
<div class="stat-value" style="color:<?= $blockedActivations > 0 ? 'var(--danger)' : 'var(--text-muted)' ?>;"><?= $blockedActivations ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtab: Licenses -->
|
||||
<div id="sub-license-licenses" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">Neuen Lizenzschlüssel generieren</h2></div>
|
||||
<div class="card-header"><h2 class="card-title">🔑 Neuen Lizenzschlüssel generieren</h2></div>
|
||||
<form method="POST" action="index.php#sub-license-licenses">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="create_license">
|
||||
@@ -1983,16 +2016,16 @@ $csrfField = Csrf::field();
|
||||
<label class="form-label">Projekt</label>
|
||||
<select name="product_id" class="form-input" required>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?> (<?= htmlspecialchars($p['slug']) ?>)</option>
|
||||
<option value="<?= (int)$p['id'] ?>"><?= e($p['name']) ?> (<?= e($p['slug']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden Name</label>
|
||||
<label class="form-label">Kunden-Name</label>
|
||||
<input type="text" name="customer_name" class="form-input" placeholder="Musterfirma GmbH">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden E-Mail</label>
|
||||
<label class="form-label">Kunden-E-Mail</label>
|
||||
<input type="email" name="customer_email" class="form-input" placeholder="kunde@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -2004,7 +2037,7 @@ $csrfField = Csrf::field();
|
||||
<input type="date" name="expires_at" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Lizenz Generieren</button>
|
||||
<button type="submit" class="btn">Lizenz generieren</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2023,81 +2056,77 @@ $csrfField = Csrf::field();
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($licenses as $l): ?>
|
||||
<?php if ($licenses === []): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($l['product_name']) ?></td>
|
||||
<td><code style="font-size:0.9rem; font-weight:bold; color:var(--primary);"><?= htmlspecialchars($l['license_key']) ?></code></td>
|
||||
<td><?= htmlspecialchars($l['customer_name'] ?? '-') ?></td>
|
||||
<td><?= $l['active_count'] ?> / <?= $l['max_activations'] ?></td>
|
||||
<td><span class="badge badge-<?= $l['status'] === 'active' ? 'up' : 'down' ?>"><?= strtoupper($l['status']) ?></span></td>
|
||||
<td><?= $l['expires_at'] ? htmlspecialchars($l['expires_at']) : 'Unbefristet' ?></td>
|
||||
<td style="display:flex; gap:0.35rem;">
|
||||
<a href="index.php?action=download_lic&id=<?= $l['id'] ?>" class="btn btn-sm btn-secondary" title=".lic Datei herunterladen">📥 .lic</a>
|
||||
<button class="btn btn-sm btn-secondary" onclick="openEditLicense(<?= htmlspecialchars(json_encode($l)) ?>)">✏️ Edit</button>
|
||||
<td colspan="7" style="text-align:center; padding:2rem; color:var(--text-muted);">
|
||||
Noch keine Lizenzen angelegt.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($licenses as $l): ?>
|
||||
<?php
|
||||
$used = (int)$l['active_count'];
|
||||
$max = (int)$l['max_activations'];
|
||||
$full = $max > 0 && $used >= $max;
|
||||
$licJson = e((string)json_encode($l, JSON_INVALID_UTF8_SUBSTITUTE));
|
||||
?>
|
||||
<tr>
|
||||
<td><?= e($l['product_name']) ?></td>
|
||||
<td><code style="font-size:0.85rem; font-weight:bold; color:var(--primary);"><?= e($l['license_key']) ?></code></td>
|
||||
<td>
|
||||
<?= e($l['customer_name'] ?? '—') ?>
|
||||
<?php if (!empty($l['customer_email'])): ?>
|
||||
<div style="font-size:0.72rem; color:var(--text-muted);"><?= e($l['customer_email']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $full ? 'badge-warning' : 'badge-stopped' ?>">
|
||||
<?= $used ?> / <?= $max ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-<?= $l['status'] === 'active' ? 'up' : 'down' ?>">
|
||||
<?= e(strtoupper((string)$l['status'])) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:0.8rem;">
|
||||
<?= !empty($l['expires_at']) ? e(dc_dt((string)$l['expires_at'], 'd.m.Y')) : 'Unbefristet' ?>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex; gap:0.35rem; flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-sm btn-secondary"
|
||||
onclick="openEditLicense(<?= $licJson ?>)">✏️ Bearbeiten</button>
|
||||
<a href="index.php?action=download_lic&id=<?= (int)$l['id'] ?>"
|
||||
class="btn btn-sm btn-secondary" title="Signierte Offline-Lizenzdatei herunterladen">📥 .lic</a>
|
||||
<form method="POST" action="index.php#sub-license-licenses" style="display:inline">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="toggle_license_status">
|
||||
<input type="hidden" name="license_id" value="<?= (int)$l['id'] ?>">
|
||||
<input type="hidden" name="new_status" value="<?= $l['status'] === 'active' ? 'revoked' : 'active' ?>">
|
||||
<button type="submit" class="btn btn-sm <?= $l['status'] === 'active' ? 'btn-danger' : 'btn' ?>">
|
||||
<?= $l['status'] === 'active' ? 'Widerrufen' : 'Aktivieren' ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtab: License Details & Full Editing -->
|
||||
<div id="sub-license-details" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">✏️ Lizenz Vollständig Bearbeiten</h2></div>
|
||||
<form method="POST" action="index.php#sub-license-details">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="edit_license">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Lizenz auswählen</label>
|
||||
<select name="license_id" id="licEditSelect" class="form-input" required onchange="loadLicenseData(this)">
|
||||
<?php foreach ($licenses as $l): ?>
|
||||
<option value="<?= $l['id'] ?>" data-cname="<?= htmlspecialchars($l['customer_name'] ?? '') ?>" data-cemail="<?= htmlspecialchars($l['customer_email'] ?? '') ?>" data-status="<?= $l['status'] ?>" data-max="<?= $l['max_activations'] ?>" data-exp="<?= $l['expires_at'] ? date('Y-m-d', strtotime($l['expires_at'])) : '' ?>" data-notes="<?= htmlspecialchars($l['notes'] ?? '') ?>">
|
||||
<?= htmlspecialchars($l['license_key']) ?> (<?= htmlspecialchars($l['customer_name'] ?? 'Unbekannt') ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden Name</label>
|
||||
<input type="text" id="editCustomerName" name="customer_name" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden E-Mail</label>
|
||||
<input type="email" id="editCustomerEmail" name="customer_email" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select id="editStatus" name="status" class="form-input">
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="revoked">Widerrufen</option>
|
||||
<option value="suspended">Pausiert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max. Aktivierungen</label>
|
||||
<input type="number" id="editMaxAct" name="max_activations" class="form-input" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ablaufdatum</label>
|
||||
<input type="date" id="editExpiresAt" name="expires_at" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">Lizenzdaten Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">Aktivierte Hardware-IDs verwalten</h2></div>
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">🖥️ Aktivierte Hardware</h2>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Projekt</th>
|
||||
<th>Lizenzschlüssel</th>
|
||||
<th>Hardware-ID / Quelle</th>
|
||||
<th>Plattform / Ver.</th>
|
||||
<th>Plattform</th>
|
||||
<th>Hostname</th>
|
||||
<th>Zuletzt gesehen</th>
|
||||
<th>Status</th>
|
||||
@@ -2105,44 +2134,57 @@ $csrfField = Csrf::field();
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($activations === []): ?>
|
||||
<tr>
|
||||
<td colspan="8" style="text-align:center; padding:2rem; color:var(--text-muted);">
|
||||
Noch keine Aktivierungen.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($activations as $a): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($a['product_name']) ?></td>
|
||||
<td><code><?= htmlspecialchars($a['license_key']) ?></code></td>
|
||||
<td><?= e($a['product_name']) ?></td>
|
||||
<td><code style="font-size:0.78rem;"><?= e($a['license_key']) ?></code></td>
|
||||
<td>
|
||||
<code><?= htmlspecialchars($a['hardware_id']) ?></code>
|
||||
<code style="font-size:0.72rem; word-break:break-all;"><?= e($a['hardware_id']) ?></code>
|
||||
<?php if (!empty($a['hwid_source'])): ?>
|
||||
<br><small style="color:var(--text-muted);">Quelle: <?= htmlspecialchars($a['hwid_source']) ?></small>
|
||||
<div style="font-size:0.7rem; color:var(--text-muted);">Quelle: <?= e($a['hwid_source']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" style="background:var(--bg-card); border:1px solid var(--border-color); color:var(--text-main);">
|
||||
<?= htmlspecialchars(strtoupper($a['platform'] ?? 'WIN')) ?> (v<?= (int)($a['hwid_version'] ?? 1) ?>)
|
||||
<span class="badge badge-stopped">
|
||||
<?= e(strtoupper((string)($a['platform'] ?? 'WIN'))) ?> · v<?= (int)($a['hwid_version'] ?? 1) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($a['hostname'] ?? '-') ?></td>
|
||||
<td><?= htmlspecialchars($a['last_seen']) ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?= $a['is_blocked'] ? 'down' : 'up' ?>"><?= $a['is_blocked'] ? 'GESPERRT' : 'AKTIV' ?></span>
|
||||
<td style="font-size:0.8rem;"><?= e($a['hostname'] ?? '—') ?></td>
|
||||
<td style="font-size:0.78rem; color:var(--text-muted);" title="<?= e(dc_dt($a['last_seen'] ?? null, 'd.m.Y H:i:s')) ?>">
|
||||
<?= e(dc_ago($a['last_seen'] ?? null)) ?>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="index.php#sub-license-details" style="display:inline">
|
||||
<span class="badge badge-<?= !empty($a['is_blocked']) ? 'down' : 'up' ?>">
|
||||
<?= !empty($a['is_blocked']) ? 'GESPERRT' : 'AKTIV' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex; gap:0.35rem; flex-wrap:wrap;">
|
||||
<form method="POST" action="index.php#sub-license-licenses" style="display:inline">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="toggle_block_activation">
|
||||
<input type="hidden" name="activation_id" value="<?= $a['id'] ?>">
|
||||
<input type="hidden" name="block_state" value="<?= $a['is_blocked'] ? '0' : '1' ?>">
|
||||
<button type="submit" class="btn btn-sm <?= $a['is_blocked'] ? 'btn' : 'btn-danger' ?>">
|
||||
<?= $a['is_blocked'] ? 'Entsperren' : 'Sperren' ?>
|
||||
<input type="hidden" name="activation_id" value="<?= (int)$a['id'] ?>">
|
||||
<input type="hidden" name="block_state" value="<?= !empty($a['is_blocked']) ? '0' : '1' ?>">
|
||||
<button type="submit" class="btn btn-sm <?= !empty($a['is_blocked']) ? 'btn' : 'btn-danger' ?>">
|
||||
<?= !empty($a['is_blocked']) ? 'Entsperren' : 'Sperren' ?>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="index.php#sub-license-details" style="display:inline" onsubmit="return confirm('Möchtest du diese Aktivierung wirklich freigeben (löschen)?');">
|
||||
<form method="POST" action="index.php#sub-license-licenses" style="display:inline"
|
||||
onsubmit="return confirm('Aktivierung wirklich freigeben? Der Platz wird für ein anderes System frei.');">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="release_activation">
|
||||
<input type="hidden" name="activation_id" value="<?= $a['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" style="margin-left:4px;">
|
||||
Freigeben
|
||||
</button>
|
||||
<input type="hidden" name="activation_id" value="<?= (int)$a['id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Freigeben</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -2154,29 +2196,35 @@ $csrfField = Csrf::field();
|
||||
<!-- Subtab: Offline Licenses -->
|
||||
<div id="sub-license-offline" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">💾 Signierte Offline-Lizenzdatei erzeugen (.lic)</h2></div>
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1rem;">
|
||||
Erzeugt eine signierte <code>.lic</code> Offline-Lizenzdatei für Air-Gapped Kundensysteme.
|
||||
<div class="card-header"><h2 class="card-title">💾 Offline-Lizenzdatei (.lic)</h2></div>
|
||||
<p style="color:var(--text-muted); font-size:0.875rem; margin-bottom:1rem; max-width:70ch;">
|
||||
Für Systeme ohne Internetzugang. Die Datei wird serverseitig erzeugt und mit
|
||||
HMAC-SHA256 über den Schlüssel aus <code>security.license_key</code> signiert.
|
||||
Am schnellsten geht es über den 📥 .lic-Knopf direkt in der Lizenzliste.
|
||||
</p>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Lizenz auswählen</label>
|
||||
<select id="offlineLicSelect" class="form-input">
|
||||
<?php foreach ($licenses as $l): ?>
|
||||
<option value="<?= htmlspecialchars($l['license_key']) ?>"><?= htmlspecialchars($l['license_key']) ?> (<?= htmlspecialchars($l['customer_name'] ?? 'Unbekannt') ?>)</option>
|
||||
<option value="<?= e($l['license_key']) ?>"><?= e($l['license_key']) ?> (<?= e($l['customer_name'] ?? 'ohne Kundenangabe') ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ziel-Hardware-ID (optional)</label>
|
||||
<input type="text" id="offlineHwId" class="form-input" placeholder="HWID-88A9-99B1-CC02">
|
||||
<input type="text" id="offlineHwId" class="form-input" placeholder="2:win:a765bd47...">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" onclick="generateOfflinePayload()">Offline Payload Generieren</button>
|
||||
<button class="btn" onclick="generateOfflinePayload()">Vorschau erzeugen</button>
|
||||
|
||||
<div style="margin-top:1.5rem;">
|
||||
<label class="form-label">Generierte Offline .lic Payload (JSON)</label>
|
||||
<div class="prompt-box" id="offlineResultBox">Wählen Sie oben eine Lizenz aus und klicken Sie auf 'Offline Payload Generieren'.</div>
|
||||
<label class="form-label">Vorschau (unsigniert)</label>
|
||||
<div class="prompt-box" id="offlineResultBox">Lizenz auswählen und auf „Vorschau erzeugen“ klicken.</div>
|
||||
<p style="color:var(--text-muted); font-size:0.78rem; margin-top:0.5rem;">
|
||||
Diese Vorschau dient nur der Ansicht. Die gültige, signierte Datei liefert
|
||||
der 📥 .lic-Knopf in der Lizenzliste.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2196,13 +2244,16 @@ $csrfField = Csrf::field();
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($auditLogs === []): ?>
|
||||
<tr><td colspan="5" style="text-align:center; padding:2rem; color:var(--text-muted);">Noch keine Einträge.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($auditLogs as $log): ?>
|
||||
<tr>
|
||||
<td><?= $log['id'] ?></td>
|
||||
<td><?= $log['created_at'] ?></td>
|
||||
<td><code><?= htmlspecialchars($log['actor']) ?></code></td>
|
||||
<td><strong><?= htmlspecialchars($log['action']) ?></strong></td>
|
||||
<td><code style="font-size:0.8rem;"><?= htmlspecialchars($log['details'] ?? '-') ?></code></td>
|
||||
<td><?= (int)$log['id'] ?></td>
|
||||
<td style="font-size:0.8rem;" title="<?= e((string)$log['created_at']) ?> UTC"><?= e(dc_dt((string)$log['created_at'])) ?></td>
|
||||
<td><code style="font-size:0.78rem;"><?= e($log['actor']) ?></code></td>
|
||||
<td><strong><?= e($log['action']) ?></strong></td>
|
||||
<td><code style="font-size:0.75rem; word-break:break-all;"><?= e($log['details'] ?? '—') ?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -2210,37 +2261,51 @@ $csrfField = Csrf::field();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtab: Integration & Prompts -->
|
||||
<!-- Subtab: Integration -->
|
||||
<div id="sub-license-integration" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">🔑 Server Ed25519 Public Key & Endpunkte</h2></div>
|
||||
<div class="card-header"><h2 class="card-title">💻 Endpunkte & Anbindung</h2></div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Server Public Key (Ed25519 Base64)</label>
|
||||
<input type="text" id="pubKeyInput" readonly class="form-input" value="MCowBQYDK2VwAyEA9f8J7K2mX4vQ8n1L6s5t4r3q2p1o0n9m8l7k6j5h4g3f">
|
||||
<label class="form-label">Prüfung (öffentlich, ohne Token)</label>
|
||||
<input type="text" readonly class="form-input" value="<?= e($baseUrl) ?>/api/license/v1/validate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">API Validate Endpoint</label>
|
||||
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/validate">
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label">Deaktivierung (authentifiziert)</label>
|
||||
<input type="text" readonly class="form-input" value="<?= e($baseUrl) ?>/api/license/v1/deactivate">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtab: Settings -->
|
||||
<div id="sub-license-settings" class="subtab-content">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">⚙️ Moduleinstellungen & API Konfiguration</h2></div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">REST API Validation Endpoint (Public)</label>
|
||||
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/validate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">REST API Deactivation Endpoint (Geschützt - Auth erforderlich)</label>
|
||||
<input type="text" readonly class="form-input" value="<?= $baseUrl ?>/api/license/v1/deactivate">
|
||||
</div>
|
||||
<div class="alert" style="background:rgba(91,157,255,0.10); border:1px solid rgba(91,157,255,0.25); color:#bfdbfe; font-weight:500; font-size:0.82rem; margin-top:1.25rem;">
|
||||
Die Antwort von <code>/validate</code> trägt den Lizenzzustand im Feld
|
||||
<code>status</code> auf oberster Ebene (<code>valid</code>, <code>revoked</code>,
|
||||
<code>expired</code>, <code>not_found</code>, <code>activation_limit</code>) —
|
||||
bewusst ohne den Umschlag der übrigen API, weil bereits ausgelieferte Clients
|
||||
dieses Format erwarten.
|
||||
</div>
|
||||
|
||||
<div class="prompt-box" style="margin-top:1rem;">POST <?= e($baseUrl) ?>/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" }</div>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.82rem; margin-top:1rem;">
|
||||
Fertige Anbindung für .NET: <code>Deploymentcenter.Client</code>
|
||||
(<code>LicenseClient</code>, <code>HardwareId</code>, verschlüsselter Offline-Cache).
|
||||
Ausführlich in <code>docs/LICENSE_INTEGRATION_GUIDE.md</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-<modul>-<rest>), 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'}`;
|
||||
}
|
||||
|
||||
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') || '';
|
||||
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 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 hwId = document.getElementById('offlineHwId').value || '(beliebig)';
|
||||
|
||||
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 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-<modul>-<rest>) 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);
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.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);
|
||||
</script>
|
||||
|
||||
<!-- Bugtracker Details & Timeline Modal -->
|
||||
@@ -4357,6 +4555,70 @@ SYSTEM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Lizenz bearbeiten
|
||||
|
||||
Ersetzt die frühere Unteransicht "Lizenz Bearbeiten & HW". Dort musste
|
||||
man die gerade angeklickte Lizenz in einer Auswahlliste erneut suchen.
|
||||
-->
|
||||
<div id="licEditModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
|
||||
<div class="card" style="width:100%; max-width:620px; background:#161c28; border:1px solid var(--border-glass); max-height:90vh; overflow-y:auto;">
|
||||
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 class="card-title" style="margin:0; color:#fff;">✏️ Lizenz bearbeiten</h3>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="closeEditLicense()">✕</button>
|
||||
</div>
|
||||
|
||||
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:0.35rem;">
|
||||
<code id="licEditTitle" style="color:var(--primary);"></code>
|
||||
</p>
|
||||
<p style="font-size:0.75rem; color:var(--text-muted); margin-bottom:1.25rem;" id="licEditActivations"></p>
|
||||
|
||||
<form method="POST" action="index.php#sub-license-licenses">
|
||||
<?= $csrfField ?>
|
||||
<input type="hidden" name="action" value="edit_license">
|
||||
<input type="hidden" name="license_id" id="licEditId" value="0">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden-Name</label>
|
||||
<input type="text" name="customer_name" id="licEditCustomerName" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kunden-E-Mail</label>
|
||||
<input type="email" name="customer_email" id="licEditCustomerEmail" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" id="licEditStatus" class="form-input">
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="suspended">Pausiert</option>
|
||||
<option value="revoked">Widerrufen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max. Aktivierungen</label>
|
||||
<input type="number" name="max_activations" id="licEditMaxAct" class="form-input" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ablaufdatum (leer = unbegrenzt)</label>
|
||||
<input type="date" name="expires_at" id="licEditExpiresAt" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label class="form-label">Notizen</label>
|
||||
<textarea name="notes" id="licEditNotes" class="form-input" rows="2"
|
||||
placeholder="Vertragsnummer, Ansprechpartner, Besonderheiten..."></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1.25rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeEditLicense()">Abbrechen</button>
|
||||
<button type="submit" class="btn">Änderungen speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bugtracker Edit Modal -->
|
||||
<div id="btEditModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.75); backdrop-filter:blur(8px); z-index:9999; align-items:center; justify-content:center; padding:1rem;">
|
||||
<div class="card" style="width:100%; max-width:650px; background:#161c28; border:1px solid var(--border-glass);">
|
||||
|
||||
Reference in New Issue
Block a user