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

This commit is contained in:
2026-04-22 00:03:19 +02:00
parent 82ad817ad3
commit 1d948f0508
4 changed files with 248 additions and 6 deletions

View File

@@ -0,0 +1,25 @@
{
"title": "Börsenchecker",
"version": "0.1.0",
"description": "Grundgeruest fuer ein Nexus-Modul zur Beobachtung und Auswertung von Boersenwerten.",
"enabled_by_default": false,
"menu": [
{ "label": "Übersicht", "href": "/module/boersenchecker" }
],
"sidebar": {
"enabled": true,
"collapsible": true,
"default": "collapsed",
"items": [
{ "label": "Übersicht", "href": "/module/boersenchecker" }
]
},
"setup": {
"fields": []
},
"auth": {
"required": true,
"users": [],
"groups": []
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
?>
<div class="card">
<div class="pill">Börsenchecker</div>
<h1 style="margin-top:.75rem;">Börsenchecker</h1>
<p class="muted">
Das Modul ist im Nexus registriert und kann jetzt schrittweise um Datenquellen,
Watchlists, Kennzahlen und Benachrichtigungen erweitert werden.
</p>
<div class="card" style="margin-top:1rem; background:var(--panel-2);">
<strong>Status</strong>
<div class="muted" style="margin-top:.5rem;">
Aktuell ist dies das initiale Grundgeruest des Moduls.
</div>
</div>
<div class="card" style="margin-top:1rem; background:var(--panel-2);">
<strong>Nächste sinnvolle Ausbaustufen</strong>
<ul style="margin:.75rem 0 0 1.1rem;">
<li>Watchlist fuer Ticker und ISINs</li>
<li>Kursdaten per API einlesen</li>
<li>Performance, Tagesdelta und historische Trends anzeigen</li>
<li>Alerts fuer Schwellwerte definieren</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,91 @@
<?php
use App\Database;
use App\Repository\KeaHostMetadataRepository;
use App\Repository\KeaHostRepository;
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$module = modules()->get('kea');
$fallback = $module['db_defaults'] ?? [];
$settings = modules()->settings('kea');
$metadataFallback = is_array($module['metadata_db_defaults'] ?? null) ? $module['metadata_db_defaults'] : [];
$metadataConfig = is_array($settings['metadata_db'] ?? null)
? array_replace($metadataFallback, $settings['metadata_db'])
: $metadataFallback;
try {
$metadataRepo = null;
$pdo = modules()->modulePdo('kea', $fallback);
if (!empty($metadataConfig['driver']) && !empty($metadataConfig['dbname'])) {
try {
$metadataRepo = new KeaHostMetadataRepository(Database::createFromArray($metadataConfig));
$metadataRepo->ensureSchema();
} catch (\Throwable) {
$metadataRepo = null;
}
}
$repo = new KeaHostRepository($pdo, $metadataRepo);
$hosts = $repo->findAll(200);
$stats = [
'total' => $repo->countReservations() + $repo->countLeases(),
'reservations' => $repo->countReservations(),
'leases' => $repo->countLeases(),
'groups' => [],
'free_ips' => [],
];
foreach ($hosts as $host) {
$group = trim((string)($host['metadata']['group_name'] ?? ''));
if ($group !== '') {
$stats['groups'][$group] = ($stats['groups'][$group] ?? 0) + 1;
}
}
if ($metadataRepo !== null) {
$stats['free_ips'] = array_map(
static fn(array $ips): int => count($ips),
$metadataRepo->availableIpsByGroup(
array_merge($repo->usedIpAddresses(), $metadataRepo->desiredIps()),
4096
)
);
}
$rows = array_map(static function (array $host): array {
return [
'source' => (string)($host['source'] ?? 'reservation'),
'host_id' => (string)($host['host_id'] ?? '0'),
'display_name' => (string)($host['metadata']['device_name'] ?? $host['metadata']['real_name'] ?? $host['display_name'] ?? $host['hostname'] ?? 'Unbekannt'),
'ipv4_address' => (string)($host['ipv4_address'] ?? ''),
'dhcp_identifier' => (string)($host['dhcp_identifier'] ?? ''),
'last_seen_at' => (string)($host['last_seen_at'] ?? '-'),
'lease_expires_at' => (string)($host['lease_expires_at'] ?? '-'),
'real_name' => (string)($host['metadata']['real_name'] ?? '-'),
'location' => (string)($host['metadata']['location'] ?? '-'),
'group_name' => (string)($host['metadata']['group_name'] ?? '-'),
];
}, $hosts);
echo json_encode([
'ok' => true,
'stats' => [
'total' => (int)$stats['total'],
'reservations' => (int)$stats['reservations'],
'leases' => (int)$stats['leases'],
'groups' => count($stats['groups']),
'free_ips' => array_sum($stats['free_ips']),
],
'rows' => $rows,
'updated_at' => date('H:i:s'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode([
'ok' => false,
'error' => $e->getMessage(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
exit;

View File

@@ -11,6 +11,9 @@
<div> <div>
<h2 class="section-title">KEA DHCP Hosts</h2> <h2 class="section-title">KEA DHCP Hosts</h2>
<p>Reservierungen und aktuelle Leases aus der KEA-Datenbank.</p> <p>Reservierungen und aktuelle Leases aus der KEA-Datenbank.</p>
<p class="muted kea-refresh-state" data-kea-refresh-state>
Automatische Aktualisierung alle 5 Sekunden.
</p>
</div> </div>
<div class="setup-actions"> <div class="setup-actions">
<a class="cta-button" href="/module/kea/groups">Gruppen verwalten</a> <a class="cta-button" href="/module/kea/groups">Gruppen verwalten</a>
@@ -34,23 +37,23 @@
<div class="stats"> <div class="stats">
<div class="stat-card"> <div class="stat-card">
<span class="stat-label">Einträge</span> <span class="stat-label">Einträge</span>
<span class="stat-value"><?= e((string)($stats['total'] ?? 0)) ?></span> <span class="stat-value" data-kea-stat="total"><?= e((string)($stats['total'] ?? 0)) ?></span>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<span class="stat-label">Reservierungen</span> <span class="stat-label">Reservierungen</span>
<span class="stat-value"><?= e((string)($stats['reservations'] ?? 0)) ?></span> <span class="stat-value" data-kea-stat="reservations"><?= e((string)($stats['reservations'] ?? 0)) ?></span>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<span class="stat-label">Leases</span> <span class="stat-label">Leases</span>
<span class="stat-value"><?= e((string)($stats['leases'] ?? 0)) ?></span> <span class="stat-value" data-kea-stat="leases"><?= e((string)($stats['leases'] ?? 0)) ?></span>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<span class="stat-label">Gruppen</span> <span class="stat-label">Gruppen</span>
<span class="stat-value"><?= e((string)count($stats['groups'] ?? [])) ?></span> <span class="stat-value" data-kea-stat="groups"><?= e((string)count($stats['groups'] ?? [])) ?></span>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<span class="stat-label">Freie Gruppen-IPs</span> <span class="stat-label">Freie Gruppen-IPs</span>
<span class="stat-value"><?= e((string)array_sum($stats['free_ips'] ?? [])) ?></span> <span class="stat-value" data-kea-stat="free_ips"><?= e((string)array_sum($stats['free_ips'] ?? [])) ?></span>
</div> </div>
</div> </div>
@@ -78,7 +81,7 @@
<th>Aktion</th> <th>Aktion</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody data-kea-host-rows>
<?php if (empty($hosts)): ?> <?php if (empty($hosts)): ?>
<tr> <tr>
<td colspan="10" class="kea-empty"> <td colspan="10" class="kea-empty">
@@ -128,3 +131,98 @@
</div> </div>
</div> </div>
</section> </section>
<script>
(() => {
const rowsTarget = document.querySelector('[data-kea-host-rows]');
const stateTarget = document.querySelector('[data-kea-refresh-state]');
if (!rowsTarget) {
return;
}
const setText = (selector, value) => {
const target = document.querySelector(selector);
if (target) {
target.textContent = String(value ?? '0');
}
};
const cell = (value, className = '') => {
const td = document.createElement('td');
if (className) {
td.className = className;
}
td.textContent = value || '-';
return td;
};
const renderRows = (rows) => {
rowsTarget.textContent = '';
if (!Array.isArray(rows) || rows.length === 0) {
const tr = document.createElement('tr');
const td = cell('Keine Reservierungen oder aktiven Leases gefunden.', 'kea-empty');
td.colSpan = 10;
tr.appendChild(td);
rowsTarget.appendChild(tr);
return;
}
for (const row of rows) {
const tr = document.createElement('tr');
const source = document.createElement('td');
const pill = document.createElement('span');
pill.className = 'pill';
pill.textContent = row.source === 'lease' ? 'Lease' : 'Reservierung';
source.appendChild(pill);
tr.appendChild(source);
tr.appendChild(cell(row.display_name));
tr.appendChild(cell(row.ipv4_address, 'mono'));
tr.appendChild(cell(row.dhcp_identifier, 'mono'));
tr.appendChild(cell(row.last_seen_at));
tr.appendChild(cell(row.lease_expires_at));
tr.appendChild(cell(row.real_name));
tr.appendChild(cell(row.location));
tr.appendChild(cell(row.group_name));
const action = document.createElement('td');
const link = document.createElement('a');
link.className = 'nav-link';
link.href = `/module/kea/edit?source=${encodeURIComponent(row.source || 'reservation')}&id=${encodeURIComponent(row.host_id || '0')}`;
link.textContent = 'Bearbeiten';
action.appendChild(link);
tr.appendChild(action);
rowsTarget.appendChild(tr);
}
};
const refresh = async () => {
try {
const response = await fetch('/module/kea/data', {
headers: {Accept: 'application/json'},
cache: 'no-store',
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
throw new Error(payload.error || 'Aktualisierung fehlgeschlagen.');
}
setText('[data-kea-stat="total"]', payload.stats?.total);
setText('[data-kea-stat="reservations"]', payload.stats?.reservations);
setText('[data-kea-stat="leases"]', payload.stats?.leases);
setText('[data-kea-stat="groups"]', payload.stats?.groups);
setText('[data-kea-stat="free_ips"]', payload.stats?.free_ips);
renderRows(payload.rows);
if (stateTarget) {
stateTarget.textContent = `Automatische Aktualisierung aktiv. Zuletzt aktualisiert: ${payload.updated_at || '-'}`;
}
} catch (error) {
if (stateTarget) {
stateTarget.textContent = `Automatische Aktualisierung fehlgeschlagen: ${error.message}`;
}
}
};
window.setInterval(refresh, 5000);
})();
</script>