feat: add NetBox plugin store
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
<?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>Öffentliche Wheel-Releases</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 Release-Artefakte gefunden. Veröffentliche ein Wheel als Forgejo-Release-Asset.</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'; ?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php declare(strict_types=1); include dirname(__DIR__) . '/partials/head.php'; ?>
|
||||
<section class="login-page shell">
|
||||
<div class="login-card">
|
||||
<span class="eyebrow">Geschützter Bereich</span>
|
||||
<h1>Store-Administration</h1>
|
||||
<p>Plugins und Artefakte werden hier geprüft, bevor sie im öffentlichen Katalog erscheinen.</p>
|
||||
<?php if (($error ?? '') !== ''): ?><div class="notice error"><?= $e($error) ?></div><?php endif; ?>
|
||||
<form method="post" action="/admin/login" class="stack-form">
|
||||
<input type="hidden" name="_csrf" value="<?= $e($csrf) ?>">
|
||||
<label><span>Benutzername</span><input name="username" required autocomplete="username" autofocus></label>
|
||||
<label><span>Passwort</span><input type="password" name="password" required autocomplete="current-password"></label>
|
||||
<button class="button primary wide" type="submit">Sicher anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<?php include dirname(__DIR__) . '/partials/footer.php'; ?>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php declare(strict_types=1); include __DIR__ . '/partials/head.php'; ?>
|
||||
<section class="shell error-page">
|
||||
<span class="error-code"><?= $e($status ?? 500) ?></span>
|
||||
<h1><?= $e($title ?? 'Fehler') ?></h1>
|
||||
<p><?= $e($message ?? 'Die Anfrage konnte nicht verarbeitet werden.') ?></p>
|
||||
<a class="button primary" href="/">Zur Store-Startseite</a>
|
||||
</section>
|
||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php declare(strict_types=1); include __DIR__ . '/partials/head.php'; ?>
|
||||
<section class="hero">
|
||||
<div class="shell hero-grid">
|
||||
<div>
|
||||
<span class="eyebrow">Kuratierter Katalog</span>
|
||||
<h1>Plugins, auf die dein<br><span>NetBox vertrauen kann.</span></h1>
|
||||
<p class="hero-copy">Entdecke freigegebene Erweiterungen aus unseren Forgejo-Repositories. Jede Version wird separat geprüft, gehasht und erst nach einer Admin-Freigabe installierbar.</p>
|
||||
</div>
|
||||
<div class="hero-stat" aria-label="Katalogstatistik">
|
||||
<strong><?= $e($totalCount ?? 0) ?></strong>
|
||||
<span>freigegebene Plugins</span>
|
||||
<small>Metadaten commitgenau synchronisiert</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell catalog-section">
|
||||
<form class="filter-panel" method="get" action="/">
|
||||
<label class="search-field">
|
||||
<span>Plugins durchsuchen</span>
|
||||
<input type="search" name="q" value="<?= $e($query ?? '') ?>" placeholder="Name, Paket oder Beschreibung …">
|
||||
</label>
|
||||
<label>
|
||||
<span>Quelle</span>
|
||||
<select name="source">
|
||||
<option value="">Alle Quellen</option>
|
||||
<?php foreach (($sources ?? []) as $filterSource): ?>
|
||||
<option value="<?= $e($filterSource['slug']) ?>" <?= ($selectedSource ?? '') === $filterSource['slug'] ? 'selected' : '' ?>><?= $e($filterSource['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>NetBox-Version</span>
|
||||
<input name="netbox_version" value="<?= $e($selectedNetboxVersion ?? '') ?>" placeholder="z. B. 4.6.5" inputmode="decimal">
|
||||
</label>
|
||||
<button class="button primary" type="submit">Filtern</button>
|
||||
<?php if (($query ?? '') !== '' || ($selectedSource ?? '') !== '' || ($selectedNetboxVersion ?? '') !== ''): ?>
|
||||
<a class="button quiet" href="/">Zurücksetzen</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php if (!empty($invalidVersion)): ?>
|
||||
<p class="notice error">Die angegebene NetBox-Version ist ungültig. Der Versionsfilter wurde ignoriert.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="catalog-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Store</span>
|
||||
<h2><?= $e($count ?? 0) ?> <?= ($count ?? 0) === 1 ? 'Plugin' : 'Plugins' ?></h2>
|
||||
</div>
|
||||
<span class="muted">Nur freigegebene Katalogeinträge</span>
|
||||
</div>
|
||||
|
||||
<?php if (($plugins ?? []) === []): ?>
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon" aria-hidden="true">⌁</span>
|
||||
<h2>Keine Plugins gefunden</h2>
|
||||
<p>Ändere die Suche oder den Versionsfilter. Neu eingelesene Plugins erscheinen erst nach der Freigabe.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="plugin-grid">
|
||||
<?php foreach ($plugins as $plugin): ?>
|
||||
<?php $release = $plugin['latestRelease'] ?? null; $isWheel = is_array($release) && ($release['artifactKind'] ?? '') === 'wheel'; ?>
|
||||
<article class="plugin-card">
|
||||
<div class="card-topline">
|
||||
<span class="provider-pill"><?= $e(strtoupper((string) ($plugin['source']['provider'] ?? 'git'))) ?></span>
|
||||
<?php if ($isWheel): ?>
|
||||
<span class="status-pill success">Wheel geprüft</span>
|
||||
<?php elseif ($release): ?>
|
||||
<span class="status-pill warning">Kein installierbares Release</span>
|
||||
<?php else: ?>
|
||||
<span class="status-pill neutral">Kein installierbares Release</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h3><a href="/plugins/<?= $e(rawurlencode($plugin['slug'])) ?>"><?= $e($plugin['name']) ?></a></h3>
|
||||
<p><?= $e($plugin['summary'] ?: 'Für dieses Plugin ist noch keine Kurzbeschreibung hinterlegt.') ?></p>
|
||||
<dl class="card-meta">
|
||||
<div><dt>NetBox</dt><dd><?= $e($plugin['minNetboxVersion']) ?> – <?= $e($plugin['maxNetboxVersion']) ?></dd></div>
|
||||
<div><dt>Version</dt><dd><?= $release ? $e($release['version']) : '–' ?></dd></div>
|
||||
</dl>
|
||||
<div class="card-footer">
|
||||
<code><?= $e($plugin['packageName']) ?></code>
|
||||
<a class="arrow-link" href="/plugins/<?= $e(rawurlencode($plugin['slug'])) ?>" aria-label="Details zu <?= $e($plugin['name']) ?>">Details <span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($pages ?? 1) > 1): ?>
|
||||
<nav class="pagination" aria-label="Ergebnisseiten">
|
||||
<?php $baseQuery = ['q' => $query ?? '', 'source' => $selectedSource ?? '', 'netbox_version' => $selectedNetboxVersion ?? '']; ?>
|
||||
<?php if (($page ?? 1) > 1): ?><a href="/?<?= $e(http_build_query($baseQuery + ['page' => $page - 1])) ?>">← Zurück</a><?php endif; ?>
|
||||
<span>Seite <?= $e($page) ?> von <?= $e($pages) ?></span>
|
||||
<?php if (($page ?? 1) < $pages): ?><a href="/?<?= $e(http_build_query($baseQuery + ['page' => $page + 1])) ?>">Weiter →</a><?php endif; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php declare(strict_types=1); ?>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="shell footer-inner">
|
||||
<div>
|
||||
<strong>NetBox Plugin Store</strong>
|
||||
<p>Freigegebene Metadaten, reproduzierbar geprüfte Artefakte.</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="/api/v1/plugins/">Catalog API v1</a>
|
||||
<a href="/healthz">Systemstatus</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types=1); ?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="theme-color" content="#102a43">
|
||||
<meta name="description" content="Kuratierter Plugin-Katalog für NetBox mit geprüften Artefakten.">
|
||||
<title><?= $e($title ?? 'NetBox Plugin Store') ?></title>
|
||||
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#content">Zum Inhalt springen</a>
|
||||
<header class="site-header">
|
||||
<div class="shell header-inner">
|
||||
<a class="brand" href="/" aria-label="NetBox Plugin Store – Startseite">
|
||||
<span class="brand-mark" aria-hidden="true">N</span>
|
||||
<span><strong>NetBox</strong><small>Plugin Store</small></span>
|
||||
</a>
|
||||
<nav class="main-nav" aria-label="Hauptnavigation">
|
||||
<a class="<?= ($currentPath ?? '') === '/' ? 'active' : '' ?>" href="/">Store</a>
|
||||
<a href="/api/v1/plugins/">API</a>
|
||||
<?php if (!empty($adminEnabled)): ?>
|
||||
<a class="<?= str_starts_with((string) ($currentPath ?? ''), '/admin') ? 'active' : '' ?>" href="/admin">Admin</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main id="content">
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php declare(strict_types=1); include __DIR__ . '/partials/head.php'; ?>
|
||||
<?php $wheelReleases = array_values(array_filter($releases ?? [], static fn (array $release): bool => ($release['artifactKind'] ?? '') === 'wheel')); ?>
|
||||
<section class="detail-hero">
|
||||
<div class="shell">
|
||||
<a class="back-link" href="/">← Alle Plugins</a>
|
||||
<div class="detail-title-row">
|
||||
<div>
|
||||
<span class="eyebrow"><?= $e(strtoupper((string) ($source['provider'] ?? 'Git'))) ?> · <?= $e($source['name'] ?? '') ?></span>
|
||||
<h1><?= $e($plugin['name']) ?></h1>
|
||||
<p><?= $e($plugin['summary'] ?: $plugin['description']) ?></p>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<a class="button primary" href="<?= $e($plugin['repositoryUrl']) ?>" rel="noreferrer noopener">Repository öffnen ↗</a>
|
||||
<a class="button secondary" href="/api/v1/plugins/<?= $e(rawurlencode($plugin['slug'])) ?>/">API-Daten</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="shell detail-layout">
|
||||
<article class="readme-card">
|
||||
<div class="section-label">README · Commit <?= $e(substr((string) ($plugin['commitSha'] ?? ''), 0, 10)) ?></div>
|
||||
<div class="readme-content">
|
||||
<?php if (($plugin['readmeHtml'] ?? '') !== ''): ?>
|
||||
<?= $plugin['readmeHtml'] ?>
|
||||
<?php else: ?>
|
||||
<div class="empty-inline"><h2>Keine README gefunden</h2><p>Die synchronisierte Revision enthält keine unterstützte README-Datei.</p></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="detail-sidebar">
|
||||
<section class="side-card">
|
||||
<h2>Kompatibilität</h2>
|
||||
<dl class="side-list">
|
||||
<div><dt>NetBox</dt><dd><?= $e($plugin['minNetboxVersion']) ?> – <?= $e($plugin['maxNetboxVersion']) ?></dd></div>
|
||||
<div><dt>Python-Paket</dt><dd><code><?= $e($plugin['packageName']) ?></code></dd></div>
|
||||
<div><dt>Import</dt><dd><code><?= $e($plugin['importName']) ?></code></dd></div>
|
||||
<div><dt>Lizenz</dt><dd><?= $e($plugin['license'] ?: 'Nicht angegeben') ?></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="side-card releases-card">
|
||||
<div class="side-heading"><h2>Releases</h2><span><?= $e(count($releases ?? [])) ?></span></div>
|
||||
<?php if ($wheelReleases === []): ?>
|
||||
<div class="release-warning">
|
||||
<strong>Kein installierbares Release</strong>
|
||||
<p>Für die automatische Installation durch den Host-Agenten fehlt ein freigegebenes <code>.whl</code>-Artefakt.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (($releases ?? []) === []): ?>
|
||||
<p class="muted">Es wurde noch kein Artefakt separat freigegeben.</p>
|
||||
<?php else: ?>
|
||||
<ol class="release-list">
|
||||
<?php foreach ($releases as $release): ?>
|
||||
<li>
|
||||
<div><strong><?= $e($release['version']) ?></strong><span><?= $e($formatDate($release['publishedAt'] ?? null)) ?></span></div>
|
||||
<div class="release-tags">
|
||||
<span class="status-pill success">Wheel</span>
|
||||
<span><?= $e($formatBytes($release['artifactSize'])) ?></span>
|
||||
</div>
|
||||
<code class="hash" title="SHA-256"><?= $e(substr($release['sha256'], 0, 16)) ?>…</code>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
<?php include __DIR__ . '/partials/footer.php'; ?>
|
||||
Reference in New Issue
Block a user