sfsdf
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-23 00:21:47 +02:00
parent ac3ac0803b
commit 39bddf39e2
11 changed files with 375 additions and 71 deletions

View File

@@ -0,0 +1,14 @@
{
"eyebrow": "Modul",
"title": "Boersenchecker",
"description": "Depotverwaltung fuer Aktien, Kaufdaten, Kursverlauf und Waehrungsumrechnung.",
"actions": [
{ "label": "Zur Startseite", "href": "/", "variant": "ghost" },
{ "label": "Setup", "href": "/modules/setup/boersenchecker", "variant": "secondary" }
],
"tabs": [
{ "label": "Ueberblick", "href": "/module/boersenchecker" },
{ "label": "Depotverwaltung", "href": "/module/boersenchecker/depotverwaltung" },
{ "label": "Aktienverwaltung", "href": "/module/boersenchecker/aktienverwaltung" }
]
}

View File

@@ -1,26 +1,16 @@
<?php $ownerQuery = $isAdmin ? '?owner_sub=' . urlencode((string) $ownerSub) : ''; ?>
<?= module_shell_header('boersenchecker', [
'title' => 'Depotverwaltung',
'description' => 'Depots, Positionen und Kurs-Historien verwalten.',
'tabs' => [
['label' => 'Ueberblick', 'href' => '/module/boersenchecker'],
['label' => 'Depotverwaltung', 'href' => '/module/boersenchecker/depotverwaltung', 'active' => true],
['label' => 'Aktienverwaltung', 'href' => '/module/boersenchecker/aktienverwaltung'],
],
]) ?>
<div class="bc-app">
<div class="bc-grid-bg">
<div class="bc-shell bc-stack">
<header class="bc-hero">
<div class="bc-hero-top">
<div class="bc-hero-copy">
<div class="bc-eyebrow">Boersenchecker Modul</div>
<h1 class="bc-title">Depotverwaltung</h1>
<p class="bc-text">Depots, Positionen und Kurs-Historien verwalten. Die Waehrungsumrechnung nutzt weiterhin die bestehende FX-Logik des Mining-Checkers.</p>
</div>
<div class="bc-hero-controls">
<a class="bc-button bc-button--ghost" href="/">Zur Startseite</a>
<a class="bc-button bc-button--secondary" href="/modules/setup/boersenchecker">Setup</a>
</div>
</div>
<div class="bc-tabs">
<a class="bc-button bc-button--tab" href="/module/boersenchecker">Ueberblick</a>
<a class="bc-button bc-button--tab-active" href="/module/boersenchecker/depotverwaltung">Depotverwaltung</a>
<a class="bc-button bc-button--tab" href="/module/boersenchecker/aktienverwaltung">Aktienverwaltung</a>
</div>
</header>
<?php if ($error): ?>
<div class="bc-alert bc-alert--error"><?= e($error) ?></div>
@@ -479,3 +469,4 @@
</div>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -1,3 +1,12 @@
<?= module_shell_header('boersenchecker', [
'title' => 'Depot-Ueberblick',
'description' => 'Depots, Aktien und Kursverlaeufe in einer Oberflaeche.',
'tabs' => [
['label' => 'Ueberblick', 'href' => '/module/boersenchecker', 'active' => true],
['label' => 'Depotverwaltung', 'href' => '/module/boersenchecker/depotverwaltung'],
['label' => 'Aktienverwaltung', 'href' => '/module/boersenchecker/aktienverwaltung'],
],
]) ?>
<div class="bc-app">
<div class="bc-grid-bg">
<div class="bc-shell bc-stack" data-bc-home data-chart-endpoint="<?= e($chartEndpoint) ?>">
@@ -10,29 +19,6 @@
];
}, $positions), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?></script>
<header class="bc-hero">
<div class="bc-hero-top">
<div class="bc-hero-copy">
<div class="bc-eyebrow">Boersenchecker Modul</div>
<h1 class="bc-title">Depot-Ueberblick</h1>
<p class="bc-text">Depots, Aktien und Kursverlaeufe in einer Oberflaeche. Die Navigation folgt jetzt dem gleichen sichtbaren Prinzip wie beim Mining-Checker.</p>
</div>
<div class="bc-hero-controls">
<a class="bc-button bc-button--ghost" href="/">Zur Startseite</a>
<div class="bc-form-card">
<div class="bc-field-label">Aktives Depot</div>
<div class="bc-text" style="margin-top:8px;"><?= $selectedPortfolioId > 0 && $portfolios !== [] ? e((string) (($portfolios[array_search($selectedPortfolioId, array_column($portfolios, 'id'), true)]['name'] ?? 'Auswahl aktiv'))) : 'Kein Depot ausgewaehlt' ?></div>
</div>
</div>
</div>
<div class="bc-tabs">
<a class="bc-button bc-button--tab-active" href="/module/boersenchecker">Ueberblick</a>
<a class="bc-button bc-button--tab" href="/module/boersenchecker/depotverwaltung">Depotverwaltung</a>
<a class="bc-button bc-button--tab" href="/module/boersenchecker/aktienverwaltung">Aktienverwaltung</a>
</div>
</header>
<?php if ($error): ?>
<div class="bc-alert bc-alert--error"><?= e($error) ?></div>
<?php elseif ($notice): ?>
@@ -207,3 +193,4 @@
</div>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -1,25 +1,15 @@
<?= module_shell_header('boersenchecker', [
'title' => 'Aktienverwaltung',
'description' => 'Stammdaten der Aktien pflegen, Symbole suchen und manuelle Kurse verwalten.',
'tabs' => [
['label' => 'Ueberblick', 'href' => '/module/boersenchecker'],
['label' => 'Depotverwaltung', 'href' => '/module/boersenchecker/depotverwaltung'],
['label' => 'Aktienverwaltung', 'href' => '/module/boersenchecker/aktienverwaltung', 'active' => true],
],
]) ?>
<div class="bc-app">
<div class="bc-grid-bg">
<div class="bc-shell bc-stack">
<header class="bc-hero">
<div class="bc-hero-top">
<div class="bc-hero-copy">
<div class="bc-eyebrow">Boersenchecker Modul</div>
<h1 class="bc-title">Aktienverwaltung</h1>
<p class="bc-text">Stammdaten der Aktien pflegen, Symbole suchen und manuelle Kurse verwalten.</p>
</div>
<div class="bc-hero-controls">
<a class="bc-button bc-button--ghost" href="/">Zur Startseite</a>
<a class="bc-button bc-button--secondary" href="/modules/setup/boersenchecker">Setup</a>
</div>
</div>
<div class="bc-tabs">
<a class="bc-button bc-button--tab" href="/module/boersenchecker">Ueberblick</a>
<a class="bc-button bc-button--tab" href="/module/boersenchecker/depotverwaltung">Depotverwaltung</a>
<a class="bc-button bc-button--tab-active" href="/module/boersenchecker/aktienverwaltung">Aktienverwaltung</a>
</div>
</header>
<?php if ($error): ?>
<div class="bc-alert bc-alert--error"><?= e($error) ?></div>
@@ -155,3 +145,4 @@
</div>
</div>
</div>
<?= module_shell_footer() ?>

9
modules/kea/design.json Normal file
View File

@@ -0,0 +1,9 @@
{
"eyebrow": "Modul",
"title": "KEA DHCP",
"description": "Verwaltung von KEA DHCP Hosts und Reservierungen.",
"actions": [
{ "label": "Gruppen verwalten", "href": "/module/kea/groups", "variant": "secondary" },
{ "label": "Setup", "href": "/modules/setup/kea", "variant": "secondary" }
]
}

View File

@@ -6,6 +6,10 @@
* @var array $stats Kennzahlen fuer die Uebersicht.
*/
?>
<?= module_shell_header('kea', [
'title' => 'KEA DHCP Hosts',
'description' => 'Reservierungen und aktuelle Leases aus der KEA-Datenbank.',
]) ?>
<section class="kea-page">
<div class="section-head">
<div>
@@ -15,10 +19,6 @@
Automatische Aktualisierung alle 5 Sekunden.
</p>
</div>
<div class="setup-actions">
<a class="cta-button" href="/module/kea/groups">Gruppen verwalten</a>
<a class="nav-link" href="/modules/setup/kea">Setup</a>
</div>
</div>
<?php if ($error): ?>
@@ -226,3 +226,4 @@
window.setInterval(refresh, 5000);
})();
</script>
<?= module_shell_footer() ?>

View File

@@ -0,0 +1,15 @@
{
"eyebrow": "Modul",
"title": "Pi-hole",
"description": "Pi-hole Monitoring, Listen und Steuerung fuer zwei Instanzen.",
"actions": [
{ "label": "Zur Startseite", "href": "/", "variant": "ghost" },
{ "label": "Instanzen", "href": "/module/pihole/instances", "variant": "secondary" }
],
"tabs": [
{ "label": "Dashboard", "href": "/module/pihole" },
{ "label": "Instanzen", "href": "/module/pihole/instances" },
{ "label": "Listen", "href": "/module/pihole/lists" },
{ "label": "Queries", "href": "/module/pihole/queries" }
]
}

View File

@@ -6,10 +6,17 @@ $assets->addScript('/module/pihole/asset?file=pihole.js', 'footer', true);
$instances = module_fn('pihole', 'instances');
$hasConfig = !empty($instances);
?>
<?= module_shell_header('pihole', [
'title' => 'Pi-hole Dashboard',
'description' => 'Status, Blockings, Usage und Steuerung fuer beide Instanzen.',
'tabs' => [
['label' => 'Dashboard', 'href' => '/module/pihole', 'active' => true],
['label' => 'Instanzen', 'href' => '/module/pihole/instances'],
['label' => 'Listen', 'href' => '/module/pihole/lists'],
['label' => 'Queries', 'href' => '/module/pihole/queries'],
],
]) ?>
<div class="card pihole-page" data-pihole-page="dashboard">
<div class="pill">Pi-hole</div>
<h1 style="margin-top:.75rem;">Pi-hole Dashboard</h1>
<p class="muted">Status, Blockings, Usage und Steuerung fuer beide Instanzen.</p>
<div class="card" style="margin-top:1rem;">
<div class="pihole-section-header">
@@ -134,3 +141,4 @@ $hasConfig = !empty($instances);
<div class="pihole-error" data-instance-errors></div>
</div>
</template>
<?= module_shell_footer() ?>

View File

@@ -132,15 +132,23 @@ if ($primaryId === '') {
}
}
?>
<?= module_shell_header('pihole', [
'title' => 'Pi-hole Instanzen',
'description' => 'Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.',
'tabs' => [
['label' => 'Dashboard', 'href' => '/module/pihole'],
['label' => 'Instanzen', 'href' => '/module/pihole/instances', 'active' => true],
['label' => 'Listen', 'href' => '/module/pihole/lists'],
['label' => 'Queries', 'href' => '/module/pihole/queries'],
],
]) ?>
<div class="card">
<div class="pill">Pi-hole</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:.75rem;">
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<h1 style="margin:0;">Instanzen</h1>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="cta-button" type="button" data-instance-new>+ Neue Instanz</button>
</div>
</div>
<p class="muted">Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.</p>
<?php if ($error): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114;">
@@ -222,3 +230,4 @@ if ($primaryId === '') {
</form>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -897,3 +897,192 @@ a {
.ip-dot.is-used {
background: color-mix(in srgb, var(--muted) 55%, var(--surface));
}
.module-shell {
color: var(--text);
}
.module-page-bg {
position: relative;
padding: 8px 0 30px;
}
.module-page-bg::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 12% 20%, color-mix(in srgb, var(--brand-accent-2) 12%, transparent), transparent 24%),
radial-gradient(circle at 90% 6%, color-mix(in srgb, var(--brand-accent-3) 12%, transparent), transparent 20%);
}
.module-page-stack {
position: relative;
display: grid;
gap: 18px;
}
.module-hero {
display: grid;
gap: 18px;
padding: 28px;
border: 1px solid var(--line);
border-radius: 28px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(245, 252, 251, 0.88)),
linear-gradient(90deg, color-mix(in srgb, var(--brand-accent) 14%, transparent), color-mix(in srgb, var(--brand-accent-2) 14%, transparent));
box-shadow: var(--shadow);
}
:root[data-theme="night"] .module-hero {
background:
linear-gradient(135deg, rgba(8, 18, 28, 0.94), rgba(15, 29, 42, 0.86)),
linear-gradient(90deg, color-mix(in srgb, var(--brand-accent) 18%, transparent), color-mix(in srgb, var(--brand-accent-2) 16%, transparent));
}
.module-hero-top {
display: grid;
gap: 16px;
grid-template-columns: minmax(0, 1.6fr) minmax(220px, 0.8fr);
align-items: start;
}
.module-hero-copy,
.module-hero-actions {
display: grid;
gap: 12px;
}
.module-title {
margin: 0;
font-size: clamp(1.75rem, 4vw, 2.9rem);
line-height: 1;
font-weight: 700;
letter-spacing: -0.03em;
}
.module-lead {
margin: 0;
color: var(--muted);
font-size: 1rem;
line-height: 1.5;
}
.module-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.module-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 16px;
padding: 12px 16px;
cursor: pointer;
text-decoration: none;
transition: 160ms ease;
font: inherit;
}
.module-button:hover {
transform: translateY(-1px);
}
.module-button--tab-active,
.module-button--primary {
background: linear-gradient(135deg, var(--brand-accent), var(--brand-accent-3));
color: #fff7fb;
font-weight: 700;
box-shadow: 0 14px 28px color-mix(in srgb, var(--brand-accent) 18%, transparent);
}
.module-button--tab,
.module-button--secondary {
background: rgba(255, 255, 255, 0.92);
color: #09111f;
font-weight: 700;
}
.module-button--ghost {
background: color-mix(in srgb, var(--brand-accent) 14%, transparent);
border-color: color-mix(in srgb, var(--brand-accent) 34%, transparent);
color: var(--brand-accent);
font-weight: 700;
}
.module-box,
.module-box-soft,
.module-box-table,
.module-box-empty {
border: 1px solid var(--line);
border-radius: 22px;
background: var(--surface);
box-shadow: 0 12px 30px rgba(1, 22, 32, 0.08);
backdrop-filter: blur(8px);
}
.module-box,
.module-box-soft,
.module-box-table {
padding: 18px 20px;
}
.module-box-soft {
background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,252,252,0.92));
}
.module-box-empty {
padding: 20px;
}
.module-box-grid {
display: grid;
gap: 16px;
}
.module-box-grid--stats {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.module-box-grid--panels {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.module-box-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.module-box-head p {
margin: 6px 0 0;
color: var(--muted);
}
.module-box-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
}
.module-box-table {
overflow: auto;
padding: 0;
}
.module-box-table > .module-box-head,
.module-box-table > .module-box-copy {
padding: 18px 20px 0;
}
@media (max-width: 980px) {
.module-hero-top {
grid-template-columns: 1fr;
}
}

View File

@@ -253,6 +253,96 @@ function module_tpl(string $module, string $name, array $data = []): void
}
}
function module_design(string $module): array
{
if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {
return [];
}
static $cache = [];
if (array_key_exists($module, $cache)) {
return $cache[$module];
}
$path = __DIR__ . '/../../modules/' . $module . '/design.json';
if (!is_file($path)) {
$cache[$module] = [];
return $cache[$module];
}
$raw = file_get_contents($path);
$decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
$cache[$module] = is_array($decoded) ? $decoded : [];
return $cache[$module];
}
function module_shell_header(string $module, array $options = []): string
{
$design = module_design($module);
$requestPath = app()->request()->path();
$title = trim((string) ($options['title'] ?? $design['title'] ?? ucfirst($module)));
$description = trim((string) ($options['description'] ?? $design['description'] ?? ''));
$eyebrow = trim((string) ($options['eyebrow'] ?? $design['eyebrow'] ?? 'Modul'));
$actions = is_array($options['actions'] ?? null) ? $options['actions'] : (is_array($design['actions'] ?? null) ? $design['actions'] : []);
$tabs = is_array($options['tabs'] ?? null) ? $options['tabs'] : (is_array($design['tabs'] ?? null) ? $design['tabs'] : []);
$html = '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">';
$html .= '<header class="module-hero">';
$html .= '<div class="module-hero-top">';
$html .= '<div class="module-hero-copy">';
$html .= '<div class="eyebrow">' . e($eyebrow) . '</div>';
$html .= '<h1 class="module-title">' . e($title) . '</h1>';
if ($description !== '') {
$html .= '<p class="module-lead">' . e($description) . '</p>';
}
$html .= '</div>';
if ($actions !== []) {
$html .= '<div class="module-hero-actions">';
foreach ($actions as $action) {
if (!is_array($action)) {
continue;
}
$label = trim((string) ($action['label'] ?? ''));
$href = trim((string) ($action['href'] ?? ''));
if ($label === '' || $href === '') {
continue;
}
$variant = trim((string) ($action['variant'] ?? 'secondary'));
$class = $variant === 'ghost' ? 'module-button module-button--ghost' : 'module-button module-button--secondary';
$html .= '<a class="' . e($class) . '" href="' . e($href) . '">' . e($label) . '</a>';
}
$html .= '</div>';
}
$html .= '</div>';
if ($tabs !== []) {
$html .= '<nav class="module-tabs" aria-label="Modulnavigation">';
foreach ($tabs as $tab) {
if (!is_array($tab)) {
continue;
}
$label = trim((string) ($tab['label'] ?? ''));
$href = trim((string) ($tab['href'] ?? ''));
if ($label === '' || $href === '') {
continue;
}
$isActive = !empty($tab['active']) || $href === $requestPath;
$class = $isActive ? 'module-button module-button--tab-active' : 'module-button module-button--tab';
$html .= '<a class="' . e($class) . '" href="' . e($href) . '">' . e($label) . '</a>';
}
$html .= '</nav>';
}
$html .= '</header>';
return $html;
}
function module_shell_footer(): string
{
return '</div></div></div>';
}
/**
* HTML Escaping Helper.
*/