Files
MrBlake 0440b6a48f
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
Improve artifact availability UX
2026-08-24 22:42:56 +02:00

187 lines
15 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'; ?>