dsfdsf
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-24 23:54:04 +02:00
parent 739e4d4c42
commit d6f09326f4
21 changed files with 355 additions and 364 deletions

View File

@@ -3,7 +3,10 @@
"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" }
],
"tabs": [
{ "label": "Hosts", "href": "/module/kea", "match_prefixes": ["/module/kea", "/module/kea/edit"] },
{ "label": "Gruppen", "href": "/module/kea/groups", "match_prefixes": ["/module/kea/groups"] }
]
}

View File

@@ -3,21 +3,6 @@
"version": "1.2.0",
"schema_version": 3,
"description": "Verwaltung von KEA DHCP Hosts und Reservierungen.",
"menu": [
{ "label": "Hosts", "href": "/module/kea" },
{ "label": "Gruppen", "href": "/module/kea/groups" },
{ "label": "Setup", "href": "/modules/setup/kea" }
],
"sidebar": {
"enabled": true,
"collapsible": true,
"default": "collapsed",
"items": [
{ "label": "Hosts", "href": "/module/kea" },
{ "label": "Gruppen", "href": "/module/kea/groups" },
{ "label": "Setup", "href": "/modules/setup/kea" }
]
},
"setup": {
"fields": [
{ "name": "db.driver", "label": "KEA DB Driver", "type": "text", "required": true, "help": "Standard-KEA-Datenbank, die auch vom KEA-Dienst selbst genutzt wird." },

View File

@@ -101,17 +101,22 @@ $metadata = is_array($host['metadata'] ?? null) ? $host['metadata'] : [];
$selectedGroup = (string)($metadata['group_name'] ?? '');
$selectedIp = (string)($metadata['desired_ip'] ?? '');
?>
<section class="kea-page">
<div class="section-head">
<div>
<h2 class="section-title">KEA Eintrag bearbeiten</h2>
<p>Zusatzdaten werden separat von der KEA-Datenbank gespeichert.</p>
<?= module_shell_header('kea', [
'title' => 'KEA Eintrag bearbeiten',
]) ?>
<div class="module-flow kea-page">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">KEA Eintrag bearbeiten</h2>
<p>Zusatzdaten werden separat von der KEA-Datenbank gespeichert.</p>
</div>
<div class="setup-actions">
<a class="module-button module-button--secondary" href="/module/kea/groups">Gruppen verwalten</a>
<a class="module-button module-button--secondary" href="/module/kea">Zurueck</a>
</div>
</div>
<div class="setup-actions">
<a class="nav-link" href="/module/kea/groups">Gruppen verwalten</a>
<a class="nav-link" href="/module/kea">Zurueck</a>
</div>
</div>
</section>
<?php if ($error): ?>
<div class="kea-message kea-message--error" role="alert">
@@ -125,11 +130,11 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
</div>
<?php endif; ?>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill"><?= ($source === 'lease') ? 'Lease' : 'Reservierung' ?></span>
<h3><?= e((string)($host['hostname'] ?: 'Unbekannt')) ?></h3>
<h2 class="module-box-title"><?= e((string)($host['hostname'] ?: 'Unbekannt')) ?></h2>
<p class="muted">
IP <?= e((string)($host['ipv4_address'] ?? '')) ?> · MAC <?= e((string)($host['dhcp_identifier'] ?? '')) ?>
</p>
@@ -190,13 +195,13 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
<a class="nav-link" href="/module/kea">Abbrechen</a>
</div>
</form>
</div>
</section>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill">Pruefungen</span>
<h3>Gerätechecks</h3>
<h2 class="module-box-title">Gerätechecks</h2>
<p class="muted">Pruefergebnisse werden in der Nexus-DHCP-Datenbank gespeichert und koennen spaeter fuer Reports genutzt werden.</p>
</div>
</div>
@@ -224,7 +229,7 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
<p class="muted">Vorbereitet fuer spaetere HTTP/Port-Erkennung. Noch nicht automatisch aktiv, damit keine ungewollten Scans laufen.</p>
</div>
</div>
</div>
</section>
<script>
(() => {
const ipsByGroup = <?= json_encode($availableIpsByGroup, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
@@ -257,4 +262,5 @@ $selectedIp = (string)($metadata['desired_ip'] ?? '');
})();
</script>
<?php endif; ?>
</section>
</div>
<?= module_shell_footer() ?>

View File

@@ -75,14 +75,19 @@ $matrixForGroup = static function (array $group) use ($usedIpLookup): array {
return $dots;
};
?>
<section class="kea-page">
<div class="section-head">
<div>
<h2 class="section-title">KEA Gruppen</h2>
<p>Gruppen und IP-Bereiche fuer DHCP-Reservierungen.</p>
<?= module_shell_header('kea', [
'title' => 'KEA Gruppen',
]) ?>
<div class="module-flow kea-page">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">KEA Gruppen</h2>
<p>Gruppen und IP-Bereiche fuer DHCP-Reservierungen.</p>
</div>
<a class="module-button module-button--secondary" href="/module/kea">Zurueck</a>
</div>
<a class="nav-link" href="/module/kea">Zurueck</a>
</div>
</section>
<?php if ($error): ?>
<div class="kea-message kea-message--error" role="alert">
@@ -93,11 +98,11 @@ $matrixForGroup = static function (array $group) use ($usedIpLookup): array {
<div class="kea-message kea-message--success"><?= e($notice) ?></div>
<?php endif; ?>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill">Gruppe</span>
<h3>Gruppe anlegen</h3>
<h2 class="module-box-title">Gruppe anlegen</h2>
</div>
</div>
<form method="post" class="kea-edit-form">
@@ -123,13 +128,13 @@ $matrixForGroup = static function (array $group) use ($usedIpLookup): array {
<button class="cta-button" type="submit">Gruppe speichern</button>
</div>
</form>
</div>
</section>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill">IP-Bereich</span>
<h3>Bereich zuweisen</h3>
<h2 class="module-box-title">Bereich zuweisen</h2>
</div>
</div>
<form method="post" class="kea-edit-form">
@@ -155,13 +160,13 @@ $matrixForGroup = static function (array $group) use ($usedIpLookup): array {
<button class="cta-button" type="submit">Bereich speichern</button>
</div>
</form>
</div>
</section>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box-table kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill">Uebersicht</span>
<h3>Gruppen und freie IPs</h3>
<h2 class="module-box-title">Gruppen und freie IPs</h2>
</div>
</div>
<div class="kea-table-wrap">
@@ -217,5 +222,6 @@ $matrixForGroup = static function (array $group) use ($usedIpLookup): array {
</tbody>
</table>
</div>
</div>
</section>
</section>
</div>
<?= module_shell_footer() ?>

View File

@@ -10,16 +10,18 @@
'title' => 'KEA DHCP Hosts',
'description' => 'Reservierungen und aktuelle Leases aus der KEA-Datenbank.',
]) ?>
<section class="kea-page">
<div class="section-head">
<div>
<h2 class="section-title">KEA DHCP Hosts</h2>
<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 class="module-flow kea-page">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">KEA DHCP Hosts</h2>
<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>
</section>
<?php if ($error): ?>
<div class="kea-message kea-message--error" role="alert">
@@ -34,34 +36,34 @@
</div>
<?php endforeach; ?>
<div class="stats">
<div class="stat-card">
<div class="module-box-grid module-box-grid--stats stats">
<section class="module-box-soft stat-card">
<span class="stat-label">Einträge</span>
<span class="stat-value" data-kea-stat="total"><?= e((string)($stats['total'] ?? 0)) ?></span>
</div>
<div class="stat-card">
</section>
<section class="module-box-soft stat-card">
<span class="stat-label">Reservierungen</span>
<span class="stat-value" data-kea-stat="reservations"><?= e((string)($stats['reservations'] ?? 0)) ?></span>
</div>
<div class="stat-card">
</section>
<section class="module-box-soft stat-card">
<span class="stat-label">Leases</span>
<span class="stat-value" data-kea-stat="leases"><?= e((string)($stats['leases'] ?? 0)) ?></span>
</div>
<div class="stat-card">
</section>
<section class="module-box-soft stat-card">
<span class="stat-label">Gruppen</span>
<span class="stat-value" data-kea-stat="groups"><?= e((string)count($stats['groups'] ?? [])) ?></span>
</div>
<div class="stat-card">
</section>
<section class="module-box-soft stat-card">
<span class="stat-label">Freie Gruppen-IPs</span>
<span class="stat-value" data-kea-stat="free_ips"><?= e((string)array_sum($stats['free_ips'] ?? [])) ?></span>
</div>
</section>
</div>
<div class="kea-panel">
<div class="kea-panel__head">
<section class="module-box-table kea-panel">
<div class="module-box-head kea-panel__head">
<div>
<span class="pill">Inventar</span>
<h3>Registrierte Geräte</h3>
<h2 class="module-box-title">Registrierte Geräte</h2>
<p class="muted">Zusatzdaten werden in der separaten Nexus-DHCP-Datenbank gespeichert.</p>
</div>
</div>
@@ -129,8 +131,8 @@
</tbody>
</table>
</div>
</div>
</section>
</section>
</div>
<script>
(() => {
const rowsTarget = document.querySelector('[data-kea-host-rows]');

View File

@@ -99,51 +99,6 @@
overflow-x: clip;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"] {
--mc-bg: #09111f;
--mc-surface: rgba(8, 15, 29, 0.76);
--mc-surface-strong: rgba(15, 23, 42, 0.94);
--mc-line: rgba(148, 163, 184, 0.18);
--mc-line-strong: rgba(255, 255, 255, 0.12);
--mc-text: #e5eef8;
--mc-text-muted: #a7b5c8;
--mc-accent: #3dd9c4;
--mc-accent-strong: #7dd3fc;
color: var(--mc-text);
background:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
radial-gradient(circle at top left, rgba(13, 148, 136, 0.16), transparent 26%),
radial-gradient(circle at top right, rgba(59, 130, 246, 0.18), transparent 22%),
linear-gradient(180deg, #04111d 0%, #0f172a 42%, #111827 100%);
background-size: 24px 24px, 24px 24px, auto, auto, auto;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"][data-module-accent="logo"] {
--mc-accent: #ed1671;
--mc-accent-strong: #06a9c8;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"][data-module-accent="pink"] {
--mc-accent: #ed1671;
--mc-accent-strong: #f6aa21;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"][data-module-accent="cyan"] {
--mc-accent: #06a9c8;
--mc-accent-strong: #8bc53f;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"][data-module-accent="orange"] {
--mc-accent: #f6aa21;
--mc-accent-strong: #ed1671;
}
#mining-checker-app .mc-grid-bg[data-module-theme="custom"][data-module-accent="green"] {
--mc-accent: #8bc53f;
--mc-accent-strong: #06a9c8;
}
#mining-checker-app .mc-shell {
width: min(1360px, calc(100% - 24px));
max-width: 100%;

View File

@@ -12,6 +12,23 @@
const fxBaseUrl = root.dataset.fxUrl || 'https://currencyapi.net';
const fxCurrenciesUrl = root.dataset.fxCurrenciesUrl || fxBaseUrl;
const fxApiKeyMask = root.dataset.fxApiKeyMask || '';
const configuredSections = (() => {
try {
const parsed = JSON.parse(root.dataset.sectionsJson || '[]');
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((section) => {
const key = section && typeof section.key === 'string' ? section.key.trim() : '';
const label = section && typeof section.label === 'string' ? section.label.trim() : '';
return key && label ? [key, label] : null;
})
.filter(Boolean);
} catch (error) {
return [];
}
})();
const initialDebugMode = (() => {
try {
return window.localStorage.getItem('mining-checker-debug-enabled') === '1';
@@ -289,12 +306,10 @@
baseline_coins_total: '',
daily_cost_amount: '',
daily_cost_currency: 'EUR',
report_currency: 'EUR',
crypto_currency: 'DOGE',
fx_max_age_hours: 3,
module_theme_mode: 'inherit',
module_theme_accent: 'teal',
preferred_currencies: ['DOGE', 'USD', 'EUR'],
report_currency: 'EUR',
crypto_currency: 'DOGE',
fx_max_age_hours: 3,
preferred_currencies: ['DOGE', 'USD', 'EUR'],
cost_plans: [],
currencies: [],
payouts: [],
@@ -585,8 +600,6 @@
report_currency: 'EUR',
crypto_currency: 'DOGE',
fx_max_age_hours: 3,
module_theme_mode: 'inherit',
module_theme_accent: 'teal',
});
const [moduleAuthForm, setModuleAuthForm] = useState({
required: true,
@@ -1017,8 +1030,6 @@
report_currency: normalized.settings.report_currency || 'EUR',
crypto_currency: normalized.settings.crypto_currency || 'DOGE',
fx_max_age_hours: normalized.settings.fx_max_age_hours || 3,
module_theme_mode: normalized.settings.module_theme_mode || 'inherit',
module_theme_accent: normalized.settings.module_theme_accent || 'teal',
});
setFxSelection(Array.isArray(normalized.settings.preferred_currencies) && normalized.settings.preferred_currencies.length
? normalized.settings.preferred_currencies
@@ -1243,8 +1254,6 @@
report_currency: settingsForm.report_currency || 'EUR',
crypto_currency: settingsForm.crypto_currency || 'DOGE',
fx_max_age_hours: settingsForm.fx_max_age_hours || 3,
module_theme_mode: settingsForm.module_theme_mode || 'inherit',
module_theme_accent: settingsForm.module_theme_accent || 'teal',
preferred_currencies: Array.isArray(currentSettings.preferred_currencies)
? currentSettings.preferred_currencies
: fxSelection,
@@ -1806,7 +1815,7 @@
setCookie('mining_checker_report_currency', '', 0);
}
const tabs = [
const tabs = configuredSections.length ? configuredSections : [
['overview', 'Ueberblick'],
['measurements', 'Messpunkte'],
['currencies', 'Waehrungen'],
@@ -1871,17 +1880,8 @@
const debugConsoleText = debugEntries
.map((entry) => `${entry.time} · ${entry.type}\n${JSON.stringify(entry, null, 2)}`)
.join('\n\n');
const moduleThemeMode = ['inherit', 'custom'].includes(String(currentSettings.module_theme_mode || ''))
? String(currentSettings.module_theme_mode)
: 'inherit';
const moduleThemeAccent = ['teal', 'logo', 'pink', 'cyan', 'orange', 'green'].includes(String(currentSettings.module_theme_accent || ''))
? String(currentSettings.module_theme_accent)
: 'teal';
return h('div', {
className: 'mc-grid-bg',
'data-module-theme': moduleThemeMode,
'data-module-accent': moduleThemeAccent,
}, [
h('div', { key: 'shell', className: 'mc-shell mc-stack' }, [
h('header', { key: 'header', className: 'mc-hero' }, [
@@ -2949,20 +2949,6 @@
selectField('Standard-FIAT-Währung', settingsForm.report_currency || 'EUR', selectableFiatCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, report_currency: value })),
selectField('Standard-Krypto-Währung', settingsForm.crypto_currency || 'DOGE', selectableCryptoCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, crypto_currency: value })),
inputField('FX maximal in Stunden wiederverwenden', 'number', String(settingsForm.fx_max_age_hours || 3), (value) => setSettingsForm({ ...settingsForm, fx_max_age_hours: value }), '0.25'),
selectField('Modul-Layout', settingsForm.module_theme_mode || 'inherit', [
{ value: 'inherit', label: 'Wie Main-Site' },
{ value: 'custom', label: 'Custom' },
], (value) => setSettingsForm({ ...settingsForm, module_theme_mode: value })),
settingsForm.module_theme_mode === 'custom'
? selectField('Custom-Farbschema', settingsForm.module_theme_accent || 'teal', [
{ value: 'teal', label: 'Mining Teal' },
{ value: 'logo', label: 'Logo Mix' },
{ value: 'pink', label: 'Pink' },
{ value: 'cyan', label: 'Cyan' },
{ value: 'orange', label: 'Orange' },
{ value: 'green', label: 'Gruen' },
], (value) => setSettingsForm({ ...settingsForm, module_theme_accent: value }))
: null,
h('button', {
type: 'submit',
className: 'mc-button mc-button--primary',

View File

@@ -0,0 +1,16 @@
{
"eyebrow": "Modul",
"title": "Mining-Checker",
"description": "Erfassung, OCR-Auswertung und Analyse von DOGE-Mining-Messwerten als eingebettetes Modul.",
"actions": [
{ "label": "Setup", "href": "/modules/setup/mining-checker", "variant": "secondary" }
],
"sections": [
{ "key": "overview", "label": "Ueberblick" },
{ "key": "measurements", "label": "Messpunkte" },
{ "key": "currencies", "label": "Waehrungen" },
{ "key": "mining", "label": "Mining" },
{ "key": "dashboards", "label": "Dashboards" },
{ "key": "settings", "label": "Settings" }
]
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
require_once dirname(__DIR__) . '/bootstrap.php';
$moduleConfig = require dirname(__DIR__) . '/config/module.php';
$design = module_design('mining-checker');
$defaultProjectKey = (string) ($moduleConfig['default_project_key'] ?? 'doge-main');
$fxConfig = (array) ($moduleConfig['fx'] ?? []);
$fxProvider = (string) ($fxConfig['provider'] ?? 'currencyapi');
@@ -13,20 +14,28 @@ $fxApiKey = (string) ($fxConfig['api_key'] ?? '');
$fxApiKeyMasked = $fxApiKey === ''
? ''
: (strlen($fxApiKey) <= 10 ? $fxApiKey : substr($fxApiKey, 0, 6) . '...' . substr($fxApiKey, -4));
$sectionsJson = json_encode(is_array($design['sections'] ?? null) ? $design['sections'] : [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$moduleCss = file_get_contents(dirname(__DIR__) . '/assets/css/app.css') ?: '';
$moduleJs = file_get_contents(dirname(__DIR__) . '/assets/js/app.js') ?: '';
$moduleJs = str_replace('</script>', '<\/script>', $moduleJs);
?>
<div class="module-host-card mining-checker-host">
<div id="mining-checker-app"
data-default-project-key="<?= e($defaultProjectKey) ?>"
data-api-base="/api/mining-checker/v1"
data-fx-provider="<?= e($fxProvider) ?>"
data-fx-url="<?= e($fxBaseUrl) ?>"
data-fx-currencies-url="<?= e($fxCurrenciesUrl) ?>"
data-fx-api-key-mask="<?= e($fxApiKeyMasked) ?>"></div>
<?= module_shell_header('mining-checker', [
'title' => 'DOGE Mining-Checker',
]) ?>
<div class="module-flow">
<div class="module-box mining-checker-host">
<div id="mining-checker-app"
data-default-project-key="<?= e($defaultProjectKey) ?>"
data-api-base="/api/mining-checker/v1"
data-fx-provider="<?= e($fxProvider) ?>"
data-fx-url="<?= e($fxBaseUrl) ?>"
data-fx-currencies-url="<?= e($fxCurrenciesUrl) ?>"
data-fx-api-key-mask="<?= e($fxApiKeyMasked) ?>"
data-sections-json="<?= e(is_string($sectionsJson) ? $sectionsJson : '[]') ?>"></div>
</div>
</div>
<style><?= $moduleCss ?></style>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script><?= $moduleJs ?></script>
<?= module_shell_footer() ?>

View File

@@ -0,0 +1,14 @@
{
"eyebrow": "Modul",
"title": "Pi Control",
"description": "Verwaltung und Steuerung von Raspberry Pis per SSH, Presets und Konsole.",
"actions": [
{ "label": "Setup", "href": "/modules/setup/pi_control", "variant": "secondary" }
],
"tabs": [
{ "label": "Ueberblick", "href": "/module/pi_control", "match_prefixes": ["/module/pi_control"] },
{ "label": "Hosts", "href": "/module/pi_control/hosts", "match_prefixes": ["/module/pi_control/hosts"] },
{ "label": "Befehle", "href": "/module/pi_control/commands", "match_prefixes": ["/module/pi_control/commands"] },
{ "label": "Konsole", "href": "/module/pi_control/console", "match_prefixes": ["/module/pi_control/console"] }
]
}

View File

@@ -2,28 +2,6 @@
"title": "Pi Control",
"version": "0.1.0",
"description": "Verwaltung und Steuerung von Raspberry Pis (SSH/Commands/Presets).",
"menu": [
{ "label": "Übersicht", "href": "/module/pi_control" },
{ "label": "Konsole", "href": "/module/pi_control/console" },
{
"label": "Settings",
"children": [
{ "label": "Hosts", "href": "/module/pi_control/hosts" },
{ "label": "Befehle", "href": "/module/pi_control/commands" }
]
}
],
"sidebar": {
"enabled": true,
"collapsible": true,
"default": "collapsed",
"items": [
{ "label": "Übersicht", "href": "/module/pi_control" },
{ "label": "Konsole", "href": "/module/pi_control/console" },
{ "label": "Hosts", "href": "/module/pi_control/hosts" },
{ "label": "Befehle", "href": "/module/pi_control/commands" }
]
},
"setup": {
"fields": [
{ "name": "use_separate_db", "label": "Eigene Modul-DB nutzen", "type": "checkbox", "required": false, "help": "Wenn aktiv, werden die DB-Daten unten verwendet. Sonst wird die Base-DB genutzt." },

View File

@@ -79,26 +79,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY COALESCE(sort_order, id) ASC, id ASC')->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="card">
<div class="pill">Pi Control</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:.75rem;">
<h1 style="margin:0;">Befehle</h1>
<button class="cta-button" type="button" data-command-new>+ Neuer Befehl</button>
</div>
<p class="muted">Verwalte vordefinierte SSH-Befehle.</p>
<?php if ($error): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114;">
<?= e($error) ?>
<?= module_shell_header('pi_control', [
'title' => 'Befehle',
]) ?>
<div class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Befehle</h2>
<p>Verwalte vordefinierte SSH-Befehle.</p>
</div>
<button class="module-button module-button--primary" type="button" data-command-new>+ Neuer Befehl</button>
</div>
<?php elseif ($notice): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:var(--accent-2);">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="grid" style="margin-top:1rem;">
<div class="card" style="background:var(--panel-2);">
<?php if ($error): ?>
<div class="setup-db-message setup-db-message--error" style="margin-top:16px;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="setup-db-message setup-db-message--success" style="margin-top:16px;">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="module-box" style="margin-top:16px;">
<strong>Vorhandene Befehle</strong>
<?php if (!$commands): ?>
<div class="muted" style="margin-top:.75rem;">Keine Befehle vorhanden.</div>
@@ -142,7 +146,7 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY COALE
<p class="muted" style="margin-top:.5rem;">Reihenfolge per Drag & Drop ändern.</p>
<?php endif; ?>
</div>
</div>
</section>
</div>
<div class="modal" data-command-modal aria-hidden="true">
@@ -174,3 +178,4 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY COALE
</form>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -418,26 +418,32 @@ function sendToActiveConsole(array $host, string $command, bool $strictHostKey):
return [false, $msg !== '' ? $msg : 'Befehl konnte nicht gesendet werden.'];
}
?>
<div class="card">
<div class="pill">Pi Control</div>
<h1 style="margin-top:.75rem;">Konsole</h1>
<p class="muted">Wähle einen Host und führe einen Befehl aus.</p>
<?php if ($error): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114;">
<?= e($error) ?>
<?= module_shell_header('pi_control', [
'title' => 'Konsole',
]) ?>
<div class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Konsole</h2>
<p>Wähle einen Host und führe einen Befehl aus.</p>
</div>
</div>
<?php elseif ($notice): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:var(--accent-2);">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="grid" style="margin-top:1rem;">
<div class="card form-card" style="background:var(--panel-2);">
<?php if ($error): ?>
<div class="setup-db-message setup-db-message--error" style="margin-top:16px;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="setup-db-message setup-db-message--success" style="margin-top:16px;">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="module-box" style="margin-top:16px;">
<strong>Live-Konsole</strong>
<div class="card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114; display:none;" data-console-error></div>
<div class="card" style="margin-top:1rem; border-color:var(--accent-2); display:none;" data-console-notice></div>
<div class="setup-db-message setup-db-message--error" style="margin-top:1rem; display:none;" data-console-error></div>
<div class="setup-db-message setup-db-message--success" style="margin-top:1rem; display:none;" data-console-notice></div>
<form method="post" class="form-grid" style="margin-top:.75rem;" data-console-form>
<label class="form-field">
<span class="muted">Host</span>
@@ -516,5 +522,6 @@ function sendToActiveConsole(array $host, string $command, bool $strictHostKey):
</div>
</div>
</div>
</div>
</section>
</div>
<?= module_shell_footer() ?>

View File

@@ -333,29 +333,34 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
return $exitCode === 0;
}
?>
<div class="card">
<div class="pill">Pi Control</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:.75rem;">
<h1 style="margin:0;">Hosts</h1>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="nav-link" type="button" data-host-check-all>Alle Hosts prüfen</button>
<button class="cta-button" type="button" data-host-new>+ Neuer Host</button>
<?= module_shell_header('pi_control', [
'title' => 'Hosts',
]) ?>
<div class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Hosts</h2>
<p>Verwalte die Raspberry Pis, die du steuern möchtest.</p>
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="module-button module-button--secondary module-button--small" type="button" data-host-check-all>Alle Hosts prüfen</button>
<button class="module-button module-button--primary" type="button" data-host-new>+ Neuer Host</button>
</div>
</div>
</div>
<p class="muted">Verwalte die Raspberry Pis, die du steuern möchtest.</p>
<?php if ($error): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:var(--accent-2);">
<?= e($notice) ?>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="setup-db-message setup-db-message--error" style="margin-top:16px;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="setup-db-message setup-db-message--success" style="margin-top:16px;">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="grid" style="margin-top:1rem;">
<div class="card form-card" style="background:var(--panel-2);">
<div class="module-box-grid module-box-grid--panels" style="margin-top:16px;">
<div class="module-box form-card">
<strong>Registrierte Hosts</strong>
<?php if (!$hosts): ?>
<div class="muted" style="margin-top:.75rem;">Keine Hosts vorhanden.</div>
@@ -416,7 +421,9 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>
</div>
<div class="modal" data-host-modal aria-hidden="true">
@@ -469,3 +476,4 @@ function hostAuthOk(array $host, bool $strictHostKey): bool
</form>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -7,26 +7,35 @@ $hostCount = (int)$pdo->query('SELECT COUNT(*) FROM ' . $table('hosts'))->fetchC
$cmdCount = (int)$pdo->query('SELECT COUNT(*) FROM ' . $table('commands'))->fetchColumn();
$runCount = (int)$pdo->query('SELECT COUNT(*) FROM ' . $table('runs'))->fetchColumn();
?>
<div class="card">
<div class="pill">Pi Control</div>
<h1 style="margin-top:.75rem;">Raspberry Pi Steuerung</h1>
<p class="muted">SSH Hosts verwalten, Befehle definieren und Aktionen ausführen.</p>
<?= module_shell_header('pi_control', [
'title' => 'Raspberry Pi Steuerung',
]) ?>
<div class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Raspberry Pi Steuerung</h2>
<p>SSH Hosts verwalten, Befehle definieren und Aktionen ausführen.</p>
</div>
</div>
</section>
<div class="grid" style="margin-top:1rem;">
<div class="card" style="background:var(--panel-2);">
<div class="module-box-grid module-box-grid--stats">
<div class="module-box-soft">
<strong>Hosts</strong>
<div class="muted" style="margin-top:.35rem;">Registriert: <?= e((string)$hostCount) ?></div>
<div style="margin-top:.75rem;"><a class="nav-link" href="/module/pi_control/hosts">Hosts verwalten</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--secondary module-button--small" href="/module/pi_control/hosts">Hosts verwalten</a></div>
</div>
<div class="card" style="background:var(--panel-2);">
<div class="module-box-soft">
<strong>Befehle</strong>
<div class="muted" style="margin-top:.35rem;">Presets: <?= e((string)$cmdCount) ?></div>
<div style="margin-top:.75rem;"><a class="nav-link" href="/module/pi_control/commands">Befehle verwalten</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--secondary module-button--small" href="/module/pi_control/commands">Befehle verwalten</a></div>
</div>
<div class="card" style="background:var(--panel-2);">
<div class="module-box-soft">
<strong>Konsole</strong>
<div class="muted" style="margin-top:.35rem;">Runs: <?= e((string)$runCount) ?></div>
<div style="margin-top:.75rem;"><a class="nav-link" href="/module/pi_control/console">Konsole öffnen</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--secondary module-button--small" href="/module/pi_control/console">Konsole öffnen</a></div>
</div>
</div>
</div>
<?= module_shell_footer() ?>

View File

@@ -2,18 +2,5 @@
"title": "Pi-hole",
"version": "0.1.0",
"description": "Pi-hole Monitoring, Listen und Steuerung fuer zwei Instanzen.",
"menu": [
{ "label": "Dashboard", "href": "/module/pihole" },
{ "label": "Instanzen", "href": "/module/pihole/instances" }
],
"sidebar": {
"enabled": true,
"collapsible": true,
"default": "collapsed",
"items": [
{ "label": "Dashboard", "href": "/module/pihole" },
{ "label": "Instanzen", "href": "/module/pihole/instances" }
]
},
"setup": { "fields": [] }
}

View File

@@ -10,16 +10,16 @@ $hasConfig = !empty($instances);
'title' => 'Pi-hole Dashboard',
'description' => 'Status, Blockings, Usage und Steuerung fuer beide Instanzen.',
]) ?>
<div class="card pihole-page" data-pihole-page="dashboard">
<div class="module-flow pihole-page" data-pihole-page="dashboard">
<div class="card" style="margin-top:1rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Hosts</strong>
<a class="nav-link" href="/module/pihole/instances">Instanzen verwalten</a>
<a class="module-button module-button--secondary module-button--small" href="/module/pihole/instances">Instanzen verwalten</a>
</div>
<?php if (!$instances): ?>
<div class="muted" style="margin-top:.75rem;">Keine Pi-hole Instanzen vorhanden. Bitte zuerst hinzufuegen.</div>
<div style="margin-top:.75rem;"><a class="cta-button" href="/module/pihole/instances">+ Neue Instanz</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--primary" href="/module/pihole/instances">+ Neue Instanz</a></div>
<?php else: ?>
<div class="pihole-list" style="margin-top:1rem;">
<?php foreach ($instances as $instance): ?>
@@ -35,32 +35,32 @@ $hasConfig = !empty($instances);
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php if (!$hasConfig): ?>
<?php return; ?>
<?php else: ?>
<div class="pihole-grid" style="margin-top:1rem;">
<div class="card pihole-stat">
<div class="module-box-grid module-box-grid--stats pihole-grid">
<div class="module-box-soft pihole-stat">
<div class="muted">DNS Queries (heute)</div>
<div class="pihole-stat-value" data-summary-dns></div>
</div>
<div class="card pihole-stat">
<div class="module-box-soft pihole-stat">
<div class="muted">Ads geblockt</div>
<div class="pihole-stat-value" data-summary-blocked></div>
<div class="pihole-stat-sub" data-summary-percent></div>
</div>
<div class="card pihole-stat">
<div class="module-box-soft pihole-stat">
<div class="muted">Unique Clients</div>
<div class="pihole-stat-value" data-summary-clients></div>
</div>
<div class="card pihole-stat">
<div class="module-box-soft pihole-stat">
<div class="muted">Status</div>
<div class="pihole-stat-value" data-summary-status></div>
</div>
</div>
<div class="card" style="margin-top:1.25rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Blocker steuern (alle Instanzen)</strong>
<span class="muted" data-summary-last-refresh>Letztes Update: </span>
@@ -77,17 +77,17 @@ $hasConfig = !empty($instances);
<button class="nav-link" data-action="disable-custom" data-instance="all">Custom</button>
</div>
</div>
</div>
</section>
<div class="card" style="margin-top:1.25rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Instanzen</strong>
<span class="muted">Einzeln steuerbar &amp; getrennte Updates</span>
</div>
<div class="pihole-instance-grid" data-instance-cards></div>
</div>
</section>
<div class="card" style="margin-top:1.25rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Usage (Aggregiert)</strong>
<span class="muted">Query-Typen und Weiterleitungen</span>
@@ -102,12 +102,12 @@ $hasConfig = !empty($instances);
<div class="pihole-list" data-forward-destinations></div>
</div>
</div>
</div>
</section>
<?php endif; ?>
</div>
<template id="pihole-instance-template">
<div class="card pihole-instance" data-instance="">
<div class="module-box-soft pihole-instance" data-instance="">
<div class="pihole-instance-header">
<div>
<strong data-instance-name></strong>

View File

@@ -136,57 +136,62 @@ if ($primaryId === '') {
'title' => 'Pi-hole Instanzen',
'description' => 'Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.',
]) ?>
<div class="card">
<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 class="module-flow">
<section class="module-box">
<div class="module-box-head">
<div>
<h2 class="module-box-title">Instanzen</h2>
<p>Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.</p>
</div>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="module-button module-button--primary" type="button" data-instance-new>+ Neue Instanz</button>
</div>
</div>
</div>
<?php if ($error): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:#ffb4a8; background:#fff5f3; color:#7a2114;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="card notice-card" style="margin-top:1rem; border-color:var(--accent-2);">
<?= e($notice) ?>
</div>
<?php endif; ?>
<div class="pihole-instance-grid" style="margin-top:1rem;">
<?php if (!$instances): ?>
<div class="card" style="padding:16px;">Keine Instanzen vorhanden.</div>
<?php else: ?>
<?php foreach ($instances as $instance): ?>
<div class="card pihole-instance-card"
data-instance-id="<?= e((string)$instance['id']) ?>"
data-name="<?= e((string)($instance['name'] ?? '')) ?>"
data-url="<?= e((string)($instance['url'] ?? '')) ?>"
data-primary="<?= !empty($instance['is_primary']) ? '1' : '0' ?>">
<div class="pihole-instance-header">
<div>
<strong><?= e((string)($instance['name'] ?? '')) ?></strong>
<div class="muted">ID: <?= e((string)($instance['id'] ?? '')) ?></div>
<div class="muted">URL: <?= e((string)($instance['url'] ?? '')) ?></div>
</div>
<?php if (!empty($instance['is_primary']) || $instance['id'] === $primaryId): ?>
<span class="pihole-status">Primaer</span>
<?php endif; ?>
</div>
<div class="pihole-card-actions">
<button class="nav-link" type="button" data-instance-edit>Bearbeiten</button>
<button class="nav-link" type="button" data-instance-test>Test Verbindung</button>
<form method="post" onsubmit="return confirm('Instanz wirklich loeschen?')">
<input type="hidden" name="delete_id" value="<?= e((string)($instance['id'] ?? '')) ?>">
<button class="nav-link" type="submit">Loeschen</button>
</form>
</div>
<div class="pihole-test-result" data-instance-result></div>
</div>
<?php endforeach; ?>
<?php if ($error): ?>
<div class="setup-db-message setup-db-message--error" style="margin-top:16px;">
<?= e($error) ?>
</div>
<?php elseif ($notice): ?>
<div class="setup-db-message setup-db-message--success" style="margin-top:16px;">
<?= e($notice) ?>
</div>
<?php endif; ?>
</div>
<div class="module-box-grid module-box-grid--panels" style="margin-top:16px;">
<?php if (!$instances): ?>
<div class="module-box-soft">Keine Instanzen vorhanden.</div>
<?php else: ?>
<?php foreach ($instances as $instance): ?>
<div class="module-box-soft pihole-instance-card"
data-instance-id="<?= e((string)$instance['id']) ?>"
data-name="<?= e((string)($instance['name'] ?? '')) ?>"
data-url="<?= e((string)($instance['url'] ?? '')) ?>"
data-primary="<?= !empty($instance['is_primary']) ? '1' : '0' ?>">
<div class="pihole-instance-header">
<div>
<strong><?= e((string)($instance['name'] ?? '')) ?></strong>
<div class="muted">ID: <?= e((string)($instance['id'] ?? '')) ?></div>
<div class="muted">URL: <?= e((string)($instance['url'] ?? '')) ?></div>
</div>
<?php if (!empty($instance['is_primary']) || $instance['id'] === $primaryId): ?>
<span class="pihole-status">Primaer</span>
<?php endif; ?>
</div>
<div class="pihole-card-actions">
<button class="module-button module-button--secondary module-button--small" type="button" data-instance-edit>Bearbeiten</button>
<button class="module-button module-button--secondary module-button--small" type="button" data-instance-test>Test Verbindung</button>
<form method="post" onsubmit="return confirm('Instanz wirklich loeschen?')">
<input type="hidden" name="delete_id" value="<?= e((string)($instance['id'] ?? '')) ?>">
<button class="module-button module-button--secondary module-button--small" type="submit">Loeschen</button>
</form>
</div>
<div class="pihole-test-result" data-instance-result></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
</div>
<div class="modal" data-instance-modal aria-hidden="true">

View File

@@ -10,15 +10,15 @@ $hasConfig = !empty($instances);
'title' => 'Listen & Domains',
'description' => 'Top-Domains, Listen-Updates und neue Eintraege auf der Primaer-Instanz.',
]) ?>
<div class="card pihole-page" data-pihole-page="lists">
<div class="module-flow pihole-page" data-pihole-page="lists">
<?php if (!$hasConfig): ?>
<div class="card" style="margin-top:1rem; border-color:var(--accent);">
<div class="module-box">
<strong>Keine Instanzen konfiguriert</strong>
<div class="muted" style="margin-top:.35rem;">Bitte zuerst eine Pi-hole Instanz hinzufuegen.</div>
<div style="margin-top:.75rem;"><a class="nav-link" href="/module/pihole/instances">Instanzen verwalten</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--secondary module-button--small" href="/module/pihole/instances">Instanzen verwalten</a></div>
</div>
<?php else: ?>
<div class="card" style="margin-top:1rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Listen-Updates</strong>
<span class="muted">Gravity / Blocklisten neu laden</span>
@@ -27,17 +27,17 @@ $hasConfig = !empty($instances);
<button class="cta-button" data-action="gravity" data-instance="primary">Listen aktualisieren (Primaer)</button>
</div>
<div class="pihole-update" data-list-update-status></div>
</div>
</section>
<div class="pihole-split" style="margin-top:1.25rem;">
<div class="card">
<div class="module-box-grid module-box-grid--panels pihole-split">
<div class="module-box">
<div class="pihole-section-header">
<strong>Top geblockte Domains (Aggregiert)</strong>
<span class="muted">Letzte Statistiken</span>
</div>
<div class="pihole-list" data-top-ads></div>
</div>
<div class="card">
<div class="module-box">
<div class="pihole-section-header">
<strong>Top erlaubte Domains (Aggregiert)</strong>
<span class="muted">Letzte Statistiken</span>
@@ -46,7 +46,7 @@ $hasConfig = !empty($instances);
</div>
</div>
<div class="card" style="margin-top:1.25rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Domainlisten erweitern</strong>
<span class="muted">Eintraege werden auf der Primaer-Instanz gesetzt</span>
@@ -66,9 +66,9 @@ $hasConfig = !empty($instances);
<button class="cta-button" type="submit">Hinzufuegen</button>
</form>
<div class="pihole-update" data-domain-status></div>
</div>
</section>
<div class="card" style="margin-top:1.25rem;">
<section class="module-box">
<div class="pihole-section-header">
<strong>Adlist-URL hinzufuegen</strong>
<span class="muted">Optional: unterstuetzt nur wenn die API den Endpunkt anbietet.</span>
@@ -81,7 +81,7 @@ $hasConfig = !empty($instances);
<button class="nav-link" type="submit">Adlist hinzufuegen</button>
</form>
<div class="pihole-update" data-adlist-status></div>
</div>
</section>
<?php endif; ?>
</div>
<?= module_shell_footer() ?>

View File

@@ -10,23 +10,23 @@ $hasConfig = !empty($instances);
'title' => 'Zugriffe & Blockings',
'description' => 'Aktuelle Blockings, Top Clients und Status pro Instanz.',
]) ?>
<div class="card pihole-page" data-pihole-page="queries">
<div class="module-flow pihole-page" data-pihole-page="queries">
<?php if (!$hasConfig): ?>
<div class="card" style="margin-top:1rem; border-color:var(--accent);">
<div class="module-box">
<strong>Keine Instanzen konfiguriert</strong>
<div class="muted" style="margin-top:.35rem;">Bitte zuerst eine Pi-hole Instanz hinzufuegen.</div>
<div style="margin-top:.75rem;"><a class="nav-link" href="/module/pihole/instances">Instanzen verwalten</a></div>
<div style="margin-top:.75rem;"><a class="module-button module-button--secondary module-button--small" href="/module/pihole/instances">Instanzen verwalten</a></div>
</div>
<?php else: ?>
<div class="pihole-split" style="margin-top:1rem;">
<div class="card">
<div class="module-box-grid module-box-grid--panels pihole-split">
<div class="module-box">
<div class="pihole-section-header">
<strong>Aktuelle Blockings</strong>
<span class="muted">Letzte geblockte Domains</span>
</div>
<div class="pihole-blocked" data-recent-blocked></div>
</div>
<div class="card">
<div class="module-box">
<div class="pihole-section-header">
<strong>Top Clients (Aggregiert)</strong>
<span class="muted">Anfragen nach Client</span>