Main update
This commit is contained in:
@@ -13,9 +13,44 @@ Ein Desktop-Modul kann zusaetzlich diese Projektdateien besitzen:
|
||||
- `api/` fuer modulinterne HTTP-Endpunkte
|
||||
- `assets/` fuer modulnahe CSS- und JS-Dateien
|
||||
- `docs/README.md` fuer modulspezifische Hinweise, API und Sonderregeln
|
||||
- `module.json` fuer Setup, Desktop-Freigabe, Widget-Funktionen und Cron-Endpunkte
|
||||
|
||||
Module-Assets werden in diesem Projekt nicht direkt aus `public/assets/apps/...` dupliziert, sondern ueber den generischen Endpoint `/module-assets/index.php` aus dem jeweiligen Modul ausgeliefert.
|
||||
|
||||
## Trennung zwischen Systemtools und Modulen
|
||||
|
||||
- `Systemtools` sind globale Verwaltungs- und Setup-Apps und nicht installierbar
|
||||
- aktuelle Beispiele: `User Management`, `User Self Management`, `Cron Tool`
|
||||
- `Module` sind installierbare Fachanwendungen
|
||||
- aktuelle Beispiele: `Mining-Checker`, `Waehrungs-Checker`, `Boersenchecker`, `Pi-hole`
|
||||
|
||||
Diese Trennung wird ueber App-Metadaten gesteuert:
|
||||
|
||||
- `app_scope`
|
||||
- `core`
|
||||
- `system_tool`
|
||||
- `module`
|
||||
- `installable`
|
||||
- `false` fuer Core- und Systemtools
|
||||
- `true` fuer installierbare Module
|
||||
|
||||
## Modul-Metadaten in `module.json`
|
||||
|
||||
Module sollen ihre Desktop-Faehigkeiten zentral in `module.json` beschreiben.
|
||||
|
||||
Wichtige Bausteine:
|
||||
|
||||
- `desktop`
|
||||
- `available`
|
||||
- `show_on_desktop`
|
||||
- `show_in_start_menu`
|
||||
- `widgets`
|
||||
- beschreibt Widget-Funktionen, die nur fuer installierte Module verfuegbar sind
|
||||
- `cron_jobs`
|
||||
- beschreibt Cron-Endpunkte fuer die zentrale Cron-Verwaltung
|
||||
|
||||
Die Desktop-Shell liest diese Angaben automatisch ein. Neue Modul-Crons und Widgets muessen deshalb nicht zusaetzlich in einer separaten globalen Liste nachgetragen werden, solange sie sauber im Manifest beschrieben sind.
|
||||
|
||||
## Globale Desktop-Standards fuer Module
|
||||
|
||||
- gemeinsame Desktop-Mechaniken wie Fenster, Tray, globale Persistenz und Debug-Infrastruktur liegen im Desktop-Core
|
||||
|
||||
@@ -23,6 +23,84 @@ if ($normalized === 'v1/chart-data') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($normalized === 'v1/cron/refresh-quotes' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
try {
|
||||
$result = module_fn('boersenchecker', 'scheduled_refresh_quotes', [
|
||||
'trigger_source' => 'cron',
|
||||
]);
|
||||
http_response_code(!empty($result['ok']) ? 200 : 500);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (\Throwable $exception) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($normalized === 'v1/widget/refresh-quotes' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
try {
|
||||
$result = module_fn('boersenchecker', 'scheduled_refresh_quotes', [
|
||||
'trigger_source' => 'widget',
|
||||
]);
|
||||
http_response_code(!empty($result['ok']) ? 200 : 500);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (\Throwable $exception) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($normalized === 'v1/widget/status' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'GET') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
try {
|
||||
$pdo = module_fn('boersenchecker', 'pdo');
|
||||
$quoteTable = module_fn('boersenchecker', 'table', 'quotes');
|
||||
$instrumentTable = module_fn('boersenchecker', 'table', 'instruments');
|
||||
$positionTable = module_fn('boersenchecker', 'table', 'positions');
|
||||
$settings = modules()->settings('boersenchecker');
|
||||
|
||||
$latestAt = $pdo->query('SELECT MAX(quoted_at) FROM ' . $quoteTable)->fetchColumn();
|
||||
$countStmt = $pdo->query(
|
||||
'SELECT COUNT(DISTINCT i.id)
|
||||
FROM ' . $positionTable . ' p
|
||||
INNER JOIN ' . $instrumentTable . ' i ON i.id = p.instrument_id
|
||||
WHERE i.symbol IS NOT NULL
|
||||
AND i.symbol <> \'\''
|
||||
);
|
||||
$trackedCount = (int) ($countStmt->fetchColumn() ?: 0);
|
||||
|
||||
$statusText = $latestAt
|
||||
? 'Letzter Kursabruf: ' . date('d.m.Y H:i', strtotime((string) $latestAt)) . ' · ' . $trackedCount . ' Titel'
|
||||
: 'Noch keine gespeicherten Kurse vorhanden.';
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'status_text' => $statusText,
|
||||
'latest_quoted_at' => $latestAt,
|
||||
'tracked_instruments' => $trackedCount,
|
||||
'report_currency' => (string) ($settings['report_currency'] ?? 'EUR'),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (\Throwable $exception) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'message' => $exception->getMessage(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
"version": "0.2.0",
|
||||
"description": "Depotverwaltung fuer Aktien, Kaufdaten, Kursverlauf und Waehrungsumrechnung.",
|
||||
"enabled_by_default": true,
|
||||
"app_scope": "module",
|
||||
"installable": true,
|
||||
"desktop": {
|
||||
"available": true,
|
||||
"show_on_desktop": true,
|
||||
"show_in_start_menu": true
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
"widget_id": "boersenchecker-refresh",
|
||||
"title": "Boersenkurse",
|
||||
"icon": "BC",
|
||||
"zone": "sidebar",
|
||||
"default_enabled": false,
|
||||
"supports_public_home": false,
|
||||
"summary": "Kurse fuer hinterlegte Depot-Positionen manuell nachziehen.",
|
||||
"content": "Widget fuer Schnellabruf des letzten Kursstatus und manuellen Marktrefresh.",
|
||||
"launch_app_id": "boersenchecker",
|
||||
"action_label": "App oeffnen",
|
||||
"widget_type": "action-panel",
|
||||
"status_api": "/api/boersenchecker/index.php?path=v1/widget/status",
|
||||
"actions": [
|
||||
{
|
||||
"action_id": "refresh-quotes",
|
||||
"label": "Kurse abrufen",
|
||||
"endpoint": "/api/boersenchecker/index.php?path=v1/widget/refresh-quotes",
|
||||
"method": "POST"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cron_jobs": [
|
||||
{
|
||||
"job_id": "refresh-quotes",
|
||||
"label": "Boersenkurse aktualisieren",
|
||||
"description": "Laedt fuer alle aktiven Depot-Positionen neue Alpha-Vantage-Kurse.",
|
||||
"endpoint_path": "/api/boersenchecker/index.php?path=v1/cron/refresh-quotes",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"trigger_source": "cron"
|
||||
},
|
||||
"default_enabled": false,
|
||||
"default_cron": "15 */6 * * 1-5",
|
||||
"lock_minutes": 20
|
||||
}
|
||||
],
|
||||
"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 Nexus-Base-DB genutzt." },
|
||||
|
||||
@@ -4,6 +4,58 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Zentrales Modul fuer Waehrungskurse, Historie, API-Abrufe und Desktop-Widget.",
|
||||
"enabled_by_default": true,
|
||||
"app_scope": "module",
|
||||
"installable": true,
|
||||
"desktop": {
|
||||
"available": true,
|
||||
"show_on_desktop": true,
|
||||
"show_in_start_menu": true
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
"widget_id": "fx-rates-refresh",
|
||||
"title": "Waehrungskurse",
|
||||
"icon": "FX",
|
||||
"zone": "sidebar",
|
||||
"default_enabled": true,
|
||||
"supports_public_home": false,
|
||||
"summary": "Aktuelle FX-Kurse pruefen und bei Bedarf manuell aktualisieren.",
|
||||
"content": "Ein API-Abruf wird nur ausgefuehrt, wenn der letzte Snapshot alt genug ist, ausser bei explizitem Force.",
|
||||
"launch_app_id": "fx-rates",
|
||||
"action_label": "App oeffnen",
|
||||
"widget_type": "fx-rates-refresh",
|
||||
"status_api": "/api/fx-rates/index.php?path=v1/status",
|
||||
"refresh_api": "/api/fx-rates/index.php?path=v1/refresh"
|
||||
}
|
||||
],
|
||||
"cron_jobs": [
|
||||
{
|
||||
"job_id": "refresh-rates",
|
||||
"label": "FX-Kurse aktualisieren",
|
||||
"description": "Laedt neue Wechselkurse, sofern der letzte Snapshot nicht mehr frisch genug ist.",
|
||||
"endpoint_path": "/api/fx-rates/index.php?path=v1/refresh",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"trigger_source": "cron"
|
||||
},
|
||||
"default_enabled": false,
|
||||
"default_cron": "0 */6 * * *",
|
||||
"lock_minutes": 15
|
||||
},
|
||||
{
|
||||
"job_id": "sync-currency-catalog",
|
||||
"label": "FX-Waehrungskatalog synchronisieren",
|
||||
"description": "Aktualisiert den internen Waehrungskatalog des Moduls.",
|
||||
"endpoint_path": "/api/fx-rates/index.php?path=v1/currency-catalog/sync",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"trigger_source": "cron"
|
||||
},
|
||||
"default_enabled": false,
|
||||
"default_cron": "30 3 * * 1",
|
||||
"lock_minutes": 30
|
||||
}
|
||||
],
|
||||
"setup": {
|
||||
"sections": {
|
||||
"database": true
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
"version": "0.3.0",
|
||||
"description": "Erfassung, OCR-Auswertung und Analyse von DOGE-Mining-Messwerten als eingebettetes Modul.",
|
||||
"enabled_by_default": true,
|
||||
"app_scope": "module",
|
||||
"installable": true,
|
||||
"desktop": {
|
||||
"available": true,
|
||||
"show_on_desktop": true,
|
||||
"show_in_start_menu": true
|
||||
},
|
||||
"setup": {
|
||||
"sections": {
|
||||
"database": true
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/bootstrap.php';
|
||||
|
||||
use App\CronAuth;
|
||||
use ModulesCore\ModuleHttp;
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
@@ -12,11 +13,15 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
}
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
ModuleHttp::requireDesktopAccess($projectRoot, true);
|
||||
$action = (string)($_GET['action'] ?? '');
|
||||
$cronAllowedActions = ['gravity', 'update', 'widget_status'];
|
||||
$isCronRequest = in_array($action, $cronAllowedActions, true) && CronAuth::isAuthorizedRequest($projectRoot);
|
||||
|
||||
if (!$isCronRequest) {
|
||||
ModuleHttp::requireDesktopAccess($projectRoot, true);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$action = (string)($_GET['action'] ?? '');
|
||||
$instances = module_fn('pihole', 'instances');
|
||||
$listsPrimaryOnly = module_fn('pihole', 'lists_primary_only');
|
||||
$moduleSettings = modules()->settings('pihole');
|
||||
@@ -36,6 +41,22 @@ $respond = function (array $data, int $status = 200): void {
|
||||
exit;
|
||||
};
|
||||
|
||||
$requireAdminAccess = static function (bool $allowCron = false) use ($isCronRequest, $respond): void {
|
||||
if ($allowCron && $isCronRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
|
||||
$groups = array_map(
|
||||
static fn (string $group): string => strtolower(trim($group, '/')),
|
||||
array_values(array_map('strval', (array) ($currentUser['groups'] ?? [])))
|
||||
);
|
||||
|
||||
if (!in_array('administrators', $groups, true)) {
|
||||
$respond(['ok' => false, 'error' => 'forbidden'], 403);
|
||||
}
|
||||
};
|
||||
|
||||
$debugPush = static function (string $label, array $payload = []): void {
|
||||
module_debug_push('pihole', array_merge(['label' => $label], $payload));
|
||||
};
|
||||
@@ -886,8 +907,31 @@ if ($action === 'dashboard') {
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'widget_status') {
|
||||
$statusText = 'Keine Pi-hole-Instanzen konfiguriert.';
|
||||
if (!empty($instances)) {
|
||||
$primaryId = $resolvePrimaryId();
|
||||
$primaryName = $primaryId !== null && isset($instances[$primaryId])
|
||||
? (string) ($instances[$primaryId]['name'] ?? $primaryId)
|
||||
: (string) array_key_first($instances);
|
||||
$statusText = sprintf(
|
||||
'%d Instanz(en) hinterlegt · Primaer: %s',
|
||||
count($instances),
|
||||
$primaryName !== '' ? $primaryName : 'n/a'
|
||||
);
|
||||
}
|
||||
|
||||
$respond([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'status_text' => $statusText,
|
||||
'instance_count' => count($instances),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'test') {
|
||||
require_admin();
|
||||
$requireAdminAccess();
|
||||
$target = (string)($payload['instance'] ?? '');
|
||||
if ($target === '') {
|
||||
$respond(['ok' => false, 'error' => 'missing_instance'], 400);
|
||||
@@ -988,7 +1032,7 @@ if ($action === 'test') {
|
||||
}
|
||||
|
||||
if ($action === 'disable') {
|
||||
require_admin();
|
||||
$requireAdminAccess();
|
||||
$debugPush('disable.request', ['payload' => $payload]);
|
||||
$minutes = (int)($payload['minutes'] ?? 0);
|
||||
$target = (string)($payload['instance'] ?? 'all');
|
||||
@@ -1021,7 +1065,7 @@ if ($action === 'disable') {
|
||||
}
|
||||
|
||||
if ($action === 'enable') {
|
||||
require_admin();
|
||||
$requireAdminAccess();
|
||||
$debugPush('enable.request', ['payload' => $payload]);
|
||||
$target = (string)($payload['instance'] ?? 'all');
|
||||
$targets = $pickInstances($target);
|
||||
@@ -1049,7 +1093,7 @@ if ($action === 'enable') {
|
||||
}
|
||||
|
||||
if ($action === 'gravity') {
|
||||
require_admin();
|
||||
$requireAdminAccess(true);
|
||||
$debugPush('gravity.request', ['payload' => $payload]);
|
||||
$actionTimeout = $resolveActionTimeout();
|
||||
$target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary');
|
||||
@@ -1075,7 +1119,7 @@ if ($action === 'gravity') {
|
||||
}
|
||||
|
||||
if ($action === 'domain_add') {
|
||||
require_admin();
|
||||
$requireAdminAccess();
|
||||
$debugPush('domain_add.request', ['payload' => $payload]);
|
||||
$domain = trim((string)($payload['domain'] ?? ''));
|
||||
$type = (string)($payload['type'] ?? 'block');
|
||||
@@ -1109,7 +1153,7 @@ if ($action === 'domain_add') {
|
||||
}
|
||||
|
||||
if ($action === 'adlist_add') {
|
||||
require_admin();
|
||||
$requireAdminAccess();
|
||||
$debugPush('adlist_add.request', ['payload' => $payload]);
|
||||
$url = trim((string)($payload['url'] ?? ''));
|
||||
if ($url === '') {
|
||||
@@ -1138,7 +1182,7 @@ if ($action === 'adlist_add') {
|
||||
}
|
||||
|
||||
if ($action === 'update') {
|
||||
require_admin();
|
||||
$requireAdminAccess(true);
|
||||
$debugPush('update.request', ['payload' => $payload]);
|
||||
$actionTimeout = $resolveActionTimeout();
|
||||
$target = (string)($payload['instance'] ?? 'primary');
|
||||
|
||||
@@ -3,6 +3,80 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Pi-hole Monitoring, Listen und Steuerung fuer mehrere Instanzen.",
|
||||
"enabled_by_default": true,
|
||||
"app_scope": "module",
|
||||
"installable": true,
|
||||
"desktop": {
|
||||
"available": true,
|
||||
"show_on_desktop": true,
|
||||
"show_in_start_menu": true
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
"widget_id": "pihole-maintenance",
|
||||
"title": "Pi-hole Wartung",
|
||||
"icon": "PH",
|
||||
"zone": "sidebar",
|
||||
"default_enabled": false,
|
||||
"supports_public_home": false,
|
||||
"summary": "Gravity-Update und Listen-Refresh direkt aus dem Desktop.",
|
||||
"content": "Schnellaktionen fuer die Pflege der Pi-hole-Instanzen.",
|
||||
"launch_app_id": "pihole",
|
||||
"action_label": "App oeffnen",
|
||||
"widget_type": "action-panel",
|
||||
"status_api": "/api/pihole/index.php?action=widget_status",
|
||||
"actions": [
|
||||
{
|
||||
"action_id": "gravity",
|
||||
"label": "Gravity aktualisieren",
|
||||
"endpoint": "/api/pihole/index.php?action=gravity",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"instance": "primary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action_id": "update",
|
||||
"label": "Pi-hole updaten",
|
||||
"endpoint": "/api/pihole/index.php?action=update",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"instance": "primary"
|
||||
},
|
||||
"variant": "secondary"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cron_jobs": [
|
||||
{
|
||||
"job_id": "gravity-primary",
|
||||
"label": "Pi-hole Gravity Update",
|
||||
"description": "Aktualisiert die Blocklisten auf der Primaer-Instanz.",
|
||||
"endpoint_path": "/api/pihole/index.php?action=gravity",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"instance": "primary",
|
||||
"trigger_source": "cron"
|
||||
},
|
||||
"default_enabled": false,
|
||||
"default_cron": "20 4 * * *",
|
||||
"lock_minutes": 30
|
||||
},
|
||||
{
|
||||
"job_id": "update-primary",
|
||||
"label": "Pi-hole Systemupdate",
|
||||
"description": "Startet ein Update der Primaer-Instanz.",
|
||||
"endpoint_path": "/api/pihole/index.php?action=update",
|
||||
"method": "POST",
|
||||
"payload": {
|
||||
"instance": "primary",
|
||||
"trigger_source": "cron"
|
||||
},
|
||||
"default_enabled": false,
|
||||
"default_cron": "50 4 * * 0",
|
||||
"lock_minutes": 45
|
||||
}
|
||||
],
|
||||
"setup": {
|
||||
"fields": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user