187 lines
15 KiB
PHP
187 lines
15 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
use NetBoxStore\Domain\Approval;
|
||
|
||
$statusLabel = static fn (string $status): string => match ($status) {
|
||
'approved' => 'Freigegeben',
|
||
'rejected' => 'Abgelehnt',
|
||
'pending' => 'Ausstehend',
|
||
default => ucfirst($status),
|
||
};
|
||
$statusClass = static fn (string $status): string => match ($status) {
|
||
'approved' => 'success',
|
||
'rejected' => 'danger',
|
||
default => 'warning',
|
||
};
|
||
include dirname(__DIR__) . '/partials/head.php';
|
||
?>
|
||
<section class="admin-hero">
|
||
<div class="shell admin-title-row">
|
||
<div>
|
||
<span class="eyebrow">Moderation & Supply Chain</span>
|
||
<h1>Store-Administration</h1>
|
||
<p>Angemeldet als <strong><?= $e($adminUser) ?></strong>. Jede Freigabe wird protokolliert.</p>
|
||
</div>
|
||
<form method="post" action="/admin/logout">
|
||
<input type="hidden" name="_csrf" value="<?= $e($csrf) ?>">
|
||
<button class="button secondary" type="submit">Abmelden</button>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="shell admin-layout">
|
||
<?php if (($ok ?? '') !== ''): ?><div class="notice success"><?= $e($ok) ?></div><?php endif; ?>
|
||
<?php if (($error ?? '') !== ''): ?><div class="notice error"><?= $e($error) ?></div><?php endif; ?>
|
||
|
||
<div class="admin-stats">
|
||
<div><strong><?= $e(count($sources ?? [])) ?></strong><span>Quellen</span></div>
|
||
<div><strong><?= $e(count($plugins ?? [])) ?></strong><span>Plugins</span></div>
|
||
<div><strong><?= $e(count($releases ?? [])) ?></strong><span>Artefakte</span></div>
|
||
<div><strong><?= $e(count(array_filter($plugins ?? [], static fn (array $item): bool => ($item['status'] ?? '') === 'pending')) + count(array_filter($releases ?? [], static fn (array $item): bool => ($item['status'] ?? '') === 'pending'))) ?></strong><span>Offen</span></div>
|
||
</div>
|
||
|
||
<section class="admin-section">
|
||
<div class="admin-section-heading">
|
||
<div><span class="eyebrow">01</span><h2>Quellen</h2></div>
|
||
<details class="create-source">
|
||
<summary class="button primary">Quelle hinzufügen</summary>
|
||
<form method="post" action="/admin/sources" class="form-grid popover-form">
|
||
<input type="hidden" name="_csrf" value="<?= $e($csrf) ?>">
|
||
<label><span>Name</span><input name="name" required maxlength="120" placeholder="Mein Forgejo"></label>
|
||
<label><span>Slug</span><input name="slug" maxlength="64" placeholder="mein-forgejo"></label>
|
||
<label><span>Provider</span><select name="provider"><option value="forgejo">Forgejo / Gitea</option><option value="github">GitHub</option></select></label>
|
||
<label><span>Owner-Typ</span><select name="owner_kind"><option value="user">Benutzer</option><option value="organization">Organisation</option><option value="auto">Automatisch</option></select></label>
|
||
<label><span>Owner</span><input name="owner" required maxlength="120" placeholder="MrBlake"></label>
|
||
<label><span>Topic</span><input name="topic" maxlength="80" value="netbox-plugin"></label>
|
||
<label class="span-2"><span>Basis-URL</span><input type="url" name="base_url" required placeholder="https://git.example.com"></label>
|
||
<label class="span-2"><span>API-URL</span><input type="url" name="api_url" placeholder="https://git.example.com/api/v1"></label>
|
||
<label><span>Token-ENV</span><input name="token_env" pattern="[A-Z][A-Z0-9_]*" placeholder="GITEA_TOKEN"></label>
|
||
<div class="span-2 form-actions"><button class="button primary" type="submit">Ausstehend anlegen</button></div>
|
||
</form>
|
||
</details>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table class="admin-table">
|
||
<thead><tr><th>Quelle</th><th>Provider / Owner</th><th>Status</th><th>Letzter Sync</th><th>Aktionen</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach (($sources ?? []) as $source): ?>
|
||
<tr>
|
||
<td><strong><?= $e($source['name']) ?></strong><small><?= $e($source['baseUrl']) ?></small></td>
|
||
<td><?= $e($source['provider']) ?> · <?= $e($source['owner']) ?><small>Wheels oder commitgebundener Source-Fallback</small></td>
|
||
<td><span class="status-pill <?= $e($statusClass($source['status'])) ?>"><?= $e($statusLabel($source['status'])) ?></span></td>
|
||
<td><?= $e($formatDate($source['lastSyncedAt'] ?? null)) ?></td>
|
||
<td><div class="action-row">
|
||
<?php if ($source['status'] !== 'approved'): ?><form method="post" action="/admin/sources/<?= $e($source['id']) ?>/approve"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button approve">Freigeben</button></form><?php endif; ?>
|
||
<?php if ($source['status'] !== 'rejected'): ?><form method="post" action="/admin/sources/<?= $e($source['id']) ?>/reject"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button reject">Ablehnen</button></form><?php endif; ?>
|
||
<?php if ($source['status'] === 'approved'): ?><form method="post" action="/admin/sources/<?= $e($source['id']) ?>/resync"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button">Synchronisieren</button></form><?php endif; ?>
|
||
</div></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="admin-section">
|
||
<div class="admin-section-heading"><div><span class="eyebrow">02</span><h2>Plugin-Kandidaten</h2></div><p>Remote-Metadaten können vor der Freigabe dauerhaft korrigiert werden.</p></div>
|
||
<div class="moderation-list">
|
||
<?php if (($plugins ?? []) === []): ?><p class="empty-row">Noch keine Kandidaten eingelesen.</p><?php endif; ?>
|
||
<?php foreach (($plugins ?? []) as $plugin): ?>
|
||
<?php $pluginErrors = Approval::pluginErrors($plugin); ?>
|
||
<details class="moderation-item" <?= $plugin['status'] === 'pending' ? 'open' : '' ?>>
|
||
<summary>
|
||
<span><strong><?= $e($plugin['name'] ?: $plugin['repositoryName']) ?></strong><small><?= $e(($plugin['source']['name'] ?? '') . ' · ' . $plugin['repositoryOwner'] . '/' . $plugin['repositoryName']) ?></small></span>
|
||
<span class="summary-status"><span class="status-pill <?= $e($statusClass($plugin['status'])) ?>"><?= $e($statusLabel($plugin['status'])) ?></span><?php if (!empty($plugin['archived'])): ?><span class="status-pill danger">Archiviert</span><?php endif; ?></span>
|
||
</summary>
|
||
<div class="moderation-body">
|
||
<?php if ($pluginErrors !== []): ?><div class="validation-box"><strong>Freigabe blockiert</strong><ul><?php foreach ($pluginErrors as $validationError): ?><li><?= $e($validationError) ?></li><?php endforeach; ?></ul></div><?php endif; ?>
|
||
<form method="post" action="/admin/plugins/<?= $e($plugin['id']) ?>/edit" class="form-grid">
|
||
<input type="hidden" name="_csrf" value="<?= $e($csrf) ?>">
|
||
<label><span>Name</span><input name="name" required maxlength="180" value="<?= $e($plugin['name']) ?>"></label>
|
||
<label><span>Slug</span><input name="slug" required maxlength="64" pattern="[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?" value="<?= $e($plugin['slug']) ?>"></label>
|
||
<label class="span-2"><span>Kurzbeschreibung</span><input name="summary" maxlength="320" value="<?= $e($plugin['summary']) ?>"></label>
|
||
<label class="span-2"><span>Beschreibung</span><textarea name="description" maxlength="65535" rows="3"><?= $e($plugin['description']) ?></textarea></label>
|
||
<label><span>Python-Paket</span><input name="package_name" required maxlength="128" value="<?= $e($plugin['packageName']) ?>"></label>
|
||
<label><span>Top-Level-Import</span><input name="import_name" required maxlength="128" value="<?= $e($plugin['importName']) ?>"></label>
|
||
<label><span>Min. NetBox</span><input name="min_netbox_version" required value="<?= $e($plugin['minNetboxVersion']) ?>"></label>
|
||
<label><span>Max. NetBox</span><input name="max_netbox_version" required value="<?= $e($plugin['maxNetboxVersion']) ?>"></label>
|
||
<div class="span-2 form-actions"><button class="button secondary" type="submit">Korrekturen speichern</button><small>Diese Overrides bleiben bei späteren Syncs erhalten.</small></div>
|
||
</form>
|
||
<div class="moderation-actions">
|
||
<?php if ($plugin['status'] !== 'approved'): ?><form method="post" action="/admin/plugins/<?= $e($plugin['id']) ?>/approve"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="button primary" <?= $pluginErrors !== [] ? 'disabled title="Metadaten zuerst korrigieren"' : '' ?>>Plugin freigeben</button></form><?php endif; ?>
|
||
<?php if ($plugin['status'] !== 'rejected'): ?><form method="post" action="/admin/plugins/<?= $e($plugin['id']) ?>/reject"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="button danger">Ablehnen</button></form><?php endif; ?>
|
||
<form method="post" action="/admin/plugins/<?= $e($plugin['id']) ?>/resync"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="button quiet">Quelle neu einlesen</button></form>
|
||
<a class="button quiet" href="<?= $e($plugin['repositoryUrl']) ?>" rel="noreferrer noopener">Repository ↗</a>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="admin-section">
|
||
<div class="admin-section-heading"><div><span class="eyebrow">03</span><h2>Release-Artefakte</h2></div><p>Neue oder veränderte Payloads sind immer ausstehend.</p></div>
|
||
<div class="table-wrap">
|
||
<table class="admin-table release-admin-table">
|
||
<thead><tr><th>Plugin / Version</th><th>Artefakt</th><th>Integrität</th><th>Status</th><th>Aktionen</th></tr></thead>
|
||
<tbody>
|
||
<?php if (($releases ?? []) === []): ?><tr><td colspan="5" class="empty-row">Keine installierbaren Artefakte gefunden. Bei Repositorys ohne Release erzeugt eine erneute Synchronisierung automatisch ein commitgebundenes Source-Artefakt, sofern das Python-Paket eine Version definiert.</td></tr><?php endif; ?>
|
||
<?php foreach (($releases ?? []) as $release): ?>
|
||
<?php $releasePlugin = $release['plugin'] ?? []; $releaseErrors = Approval::releaseErrors($releasePlugin, $release); ?>
|
||
<tr>
|
||
<td><strong><?= $e($releasePlugin['name'] ?? 'Unbekannt') ?> · <?= $e($release['version']) ?></strong><small><?= $e($release['title'] ?? '') ?></small></td>
|
||
<td>
|
||
<span class="status-pill <?= ($release['artifactKind'] ?? '') === 'wheel' ? 'success' : 'warning' ?>"><?= $e($release['artifactKind'] ?? 'source') ?></span>
|
||
<small><?= $e($formatBytes($release['artifactSize'])) ?> · <?= $e($formatDate($release['publishedAt'] ?? null)) ?></small>
|
||
<details class="artifact-details"><summary>URLs vollständig prüfen</summary>
|
||
<strong>Download</strong><?php if (($release['downloadUrl'] ?? '') !== ''): ?><a href="<?= $e($release['downloadUrl']) ?>" rel="noreferrer noopener" class="break-value"><?= $e($release['downloadUrl']) ?></a><?php else: ?><span>–</span><?php endif; ?>
|
||
<?php if (($release['releaseUrl'] ?? '') !== ''): ?><strong>Release-Seite</strong><a href="<?= $e($release['releaseUrl']) ?>" rel="noreferrer noopener" class="break-value"><?= $e($release['releaseUrl']) ?></a><?php endif; ?>
|
||
</details>
|
||
</td>
|
||
<td>
|
||
<code class="full-hash"><?= ($release['sha256'] ?? '') !== '' ? $e($release['sha256']) : 'kein Hash' ?></code>
|
||
<small>Commit: <code class="break-value"><?= $e($release['commitSha'] ?? '–') ?></code></small>
|
||
<small>NetBox: <?= $e(($release['minNetboxVersion'] ?? '') ?: ($releasePlugin['minNetboxVersion'] ?? '–')) ?> – <?= $e(($release['maxNetboxVersion'] ?? '') ?: ($releasePlugin['maxNetboxVersion'] ?? '–')) ?></small>
|
||
<?php if ($releaseErrors !== []): ?><small class="text-danger" title="<?= $e(implode(' ', $releaseErrors)) ?>"><?= $e(count($releaseErrors)) ?> Prüfproblem(e)</small><?php endif; ?>
|
||
</td>
|
||
<td><span class="status-pill <?= $e($statusClass($release['status'])) ?>"><?= $e($statusLabel($release['status'])) ?></span><?php if (!empty($release['withdrawn'])): ?><small class="text-danger">Zurückgezogen</small><?php endif; ?></td>
|
||
<td><div class="action-row">
|
||
<?php if ($release['status'] !== 'approved'): ?><form method="post" action="/admin/releases/<?= $e($release['id']) ?>/approve"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button approve" <?= $releaseErrors !== [] ? 'disabled title="Validierung fehlgeschlagen"' : '' ?>>Freigeben</button></form><?php endif; ?>
|
||
<?php if ($release['status'] !== 'rejected'): ?><form method="post" action="/admin/releases/<?= $e($release['id']) ?>/reject"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button reject">Ablehnen</button></form><?php endif; ?>
|
||
<form method="post" action="/admin/releases/<?= $e($release['id']) ?>/resync"><input type="hidden" name="_csrf" value="<?= $e($csrf) ?>"><button class="mini-button">Neu laden</button></form>
|
||
</div></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="admin-columns">
|
||
<section class="admin-section compact-section">
|
||
<div class="admin-section-heading"><div><span class="eyebrow">04</span><h2>Sync-Verlauf</h2></div></div>
|
||
<ol class="timeline">
|
||
<?php if (($runs ?? []) === []): ?><li class="empty-row">Noch kein Sync ausgeführt.</li><?php endif; ?>
|
||
<?php foreach (($runs ?? []) as $run): ?>
|
||
<li><span class="timeline-dot <?= $e($run['status']) ?>"></span><div>
|
||
<strong><?= $e($run['status']) ?></strong><p><?= $e($run['message']) ?></p>
|
||
<small><?= $e($formatDate($run['startedAt'])) ?> · <?= $e($run['candidatesFound']) ?> Kandidaten · <?= $e(count($run['errors'] ?? [])) ?> Hinweise/Fehler</small>
|
||
<?php if (($run['errors'] ?? []) !== []): ?><details class="sync-errors"><summary>Details anzeigen</summary><ul>
|
||
<?php foreach ($run['errors'] as $item): ?><li><strong><?= $e($item['repository'] ?? $item['source'] ?? 'Source') ?>:</strong> <?= $e($item['error'] ?? 'Unbekannter Fehler') ?></li><?php endforeach; ?>
|
||
</ul></details><?php endif; ?>
|
||
</div></li>
|
||
<?php endforeach; ?>
|
||
</ol>
|
||
</section>
|
||
<section class="admin-section compact-section">
|
||
<div class="admin-section-heading"><div><span class="eyebrow">05</span><h2>Audit-Log</h2></div></div>
|
||
<ol class="timeline">
|
||
<?php if (($audits ?? []) === []): ?><li class="empty-row">Noch keine Admin-Aktion protokolliert.</li><?php endif; ?>
|
||
<?php foreach (($audits ?? []) as $audit): ?><li><span class="timeline-dot audit"></span><div><strong><?= $e($audit['actor']) ?> · <?= $e($audit['action']) ?></strong><p><?= $e($audit['targetType']) ?> · <code><?= $e(substr($audit['targetId'], 0, 12)) ?></code></p><small><?= $e($formatDate($audit['timestamp'])) ?></small></div></li><?php endforeach; ?>
|
||
</ol>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
<?php include dirname(__DIR__) . '/partials/footer.php'; ?>
|