From d944ee84d8c6039f6d51b4a2a202f0200559ea11 Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Sun, 22 Mar 2026 10:43:36 +0100 Subject: [PATCH] Update Prozess eingebaut --- README.md | 12 + docs/installationshandbuch.md | 32 +- saas-app/README.md | 24 + saas-app/public/admin.php | 218 +++++- saas-app/public/admin/updates/index.php | 5 + saas-app/public/app-support.php | 679 +++++++++++++++++- saas-app/public/exports/index.php | 7 + saas-app/public/index.php | 182 ++++- saas-app/public/install-support.php | 259 +++++++ saas-app/public/install.php | 10 + saas-app/public/settings/index.php | 7 + ...add_tenant_settings_and_license_tables.php | 84 +++ ...000002_seed_license_plans_and_features.php | 79 ++ ...2_000003_backfill_default_team_license.php | 25 + ...026_03_22_000004_add_print_list_tables.php | 41 ++ saas-app/version.php | 3 + scripts/run-updates.php | 19 + 17 files changed, 1636 insertions(+), 50 deletions(-) create mode 100644 saas-app/public/admin/updates/index.php create mode 100644 saas-app/public/exports/index.php create mode 100644 saas-app/public/settings/index.php create mode 100644 saas-app/updates/2026_03_22_000001_add_tenant_settings_and_license_tables.php create mode 100644 saas-app/updates/2026_03_22_000002_seed_license_plans_and_features.php create mode 100644 saas-app/updates/2026_03_22_000003_backfill_default_team_license.php create mode 100644 saas-app/updates/2026_03_22_000004_add_print_list_tables.php create mode 100644 saas-app/version.php create mode 100644 scripts/run-updates.php diff --git a/README.md b/README.md index cc1d596..830b702 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,17 @@ Nach dem Setup gibt es jetzt zwei zentrale Web-Einstiege: - `saas-app/public/admin/login/` fuer den Global-Admin - `saas-app/public/admin/` fuer die zentrale Verwaltung +- `saas-app/public/admin/updates/` fuer den webbasierten Update-Prozess + +## SaaS-Betrieb + +Die aktuelle Ausbaustufe enthaelt bereits die wichtigsten SaaS-Bausteine: + +- Lizenzplaene pro Mandant mit Funktionsfreischaltung +- Mandanten-Einstellungen fuer Preise, PDF-Listen und Kommunikation +- Drucklisten als PDF-Ansicht plus Nacherfassung von Vorder- und Rueckseite +- Basis-Exporte fuer Mitglieder und Ledger als CSV +- Global-Admin-Zugriff zum direkten Oeffnen eines Mandanten ## Hilfsskripte @@ -44,6 +55,7 @@ Nach dem Setup gibt es jetzt zwei zentrale Web-Einstiege: - `scripts/install-saas.php` fuehrt den lokalen Setup-Grundlauf aus. - `scripts/build-migration-bundle.php` baut die SQL-Migrationen zu einer Datei. - `scripts/run-sql-migrations.php` fuehrt die SQL-Migrationen direkt per PDO fuer den konfigurierten DB-Treiber aus. +- `scripts/run-updates.php` fuehrt nachtraegliche System-Updates per PHP aus. ## Hinweise Zum Umbau diff --git a/docs/installationshandbuch.md b/docs/installationshandbuch.md index 6b7f941..2dddd48 100644 --- a/docs/installationshandbuch.md +++ b/docs/installationshandbuch.md @@ -16,6 +16,7 @@ Der Installer kann: - die `.env` speichern - das SQL-Bundle erzeugen - Migrationen direkt per PHP ausfuehren, wenn die zum Treiber passende PDO-Erweiterung verfuegbar ist +- nachtraegliche System-Updates automatisch einspielen - den ersten Global-Admin direkt anlegen - sich nach erfolgreicher Einrichtung sperren @@ -93,7 +94,7 @@ eigene Umgebung angepasst werden: 4. Sicherstellen, dass `open_basedir` nicht nur auf `public/` eingeschraenkt ist. PHP muss mindestens auf den kompletten Ordner `saas-app/` zugreifen duerfen, obwohl der Document-Root auf `public/` zeigt. 5. Den Installer unter `/install/` aufrufen und die Einrichtung durchfuehren. 6. Danach den Global-Admin unter `/admin/login` anmelden. -7. In der zentralen Verwaltung unter `/admin/` die ersten Tenants anlegen. +7. In der zentralen Verwaltung unter `/admin/tenants` die ersten Tenants inklusive Lizenzplan anlegen. 8. Nach erfolgreicher Einrichtung den Installer sperren. 9. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen. @@ -131,6 +132,35 @@ Nach der Installation stehen diese Web-Einstiege bereit: - `/admin/login` fuer den Global-Admin - `/admin/` fuer die zentrale Verwaltungsuebersicht - `/admin/tenants` fuer die Tenant-Verwaltung +- `/admin/updates` fuer System-Updates und Datenbankerweiterungen + +## Lizenzplaene + +Die SaaS-Anwendung arbeitet jetzt mit Lizenzplaenen pro Mandant. Darueber kann +gesteuert werden, welche Zusatzfunktionen im jeweiligen Tenant verfuegbar sind. + +- `Starter`: Basisfunktionen fuer Mitglieder, Buchungen, Einzahlungen und Inhalte +- `Team`: zusaetzlich Mandanten-Einstellungen, PDF-Drucklisten, Papierlisten-Erfassung und Basis-Exporte +- `Business`: vorbereitet fuer SSO, Importe, Exporte, Benachrichtigungen und Auswertungen +- `Enterprise`: erweitert um White-Labeling, Sonderfunktionen und priorisierte Updates + +Mandanten-Einstellungen und Exportfunktionen muessen daher nicht fuer jede +Lizenz freigeschaltet sein. + +## Update-Prozess + +Schemaaenderungen und spaetere Datenanpassungen laufen ueber versionierte +PHP-Update-Dateien unter `saas-app/updates/`. + +Der bevorzugte Weg auf Webspace ist: + +1. Als Global-Admin anmelden. +2. `/admin/updates` aufrufen. +3. Ausstehende Updates per Klick ausfuehren. + +Alternativ steht lokal oder auf Servern mit Shell-Zugriff weiter zur Verfuegung: + +- `php scripts/run-updates.php` ## Betriebscheck diff --git a/saas-app/README.md b/saas-app/README.md index 418da8e..def69f2 100644 --- a/saas-app/README.md +++ b/saas-app/README.md @@ -52,6 +52,30 @@ Nach der Installation erfolgt die zentrale Administration ueber: - `public/admin/login/` - `public/admin/` - `public/admin/tenants/` +- `public/admin/updates/` + +## Lizenzen und Mandantenbetrieb + +Mandanten koennen jetzt direkt mit einem Lizenzplan angelegt werden. Der +Lizenzplan steuert, welche Bereiche im Tenant sichtbar und nutzbar sind. + +- `Starter`: Basisfunktionen fuer Mitglieder, Ledger, Einzahlungen und Inhalte +- `Team`: zusaetzlich Mandanten-Einstellungen, PDF-Listen, Papierlisten-Erfassung und Basis-Exporte +- `Business`: vorbereitet fuer SSO, Importe, Exporte, Benachrichtigungen und Auswertungen +- `Enterprise`: erweitert um White-Labeling, Sonderfunktionen und priorisierte Updates + +Der Global-Admin kann jeden Mandanten oeffnen und ihn mit erweiterten Rechten +aus Sicht des jeweiligen Tenant-Admins pruefen. + +## Update-Prozess + +Nachtraegliche Schema- und Datenupdates laufen versioniert ueber: + +- `public/admin/updates/` im Browser +- `php ../scripts/run-updates.php` per CLI + +Die Update-Dateien liegen in `updates/` und werden in `app_updates` +protokolliert. ## Hosting-Hinweise diff --git a/saas-app/public/admin.php b/saas-app/public/admin.php index c83a010..f31e7be 100644 --- a/saas-app/public/admin.php +++ b/saas-app/public/admin.php @@ -22,7 +22,27 @@ function admin_badge(string $label, string $tone = 'neutral'): string return '' . admin_h($label) . ''; } -function admin_summary_metrics(array $tenants): array +function admin_update_summary(array $items): array +{ + $summary = [ + 'pending' => 0, + 'success' => 0, + 'failed' => 0, + 'running' => 0, + ]; + + foreach ($items as $item) { + $status = (string) ($item['status'] ?? 'pending'); + + if (array_key_exists($status, $summary)) { + $summary[$status]++; + } + } + + return $summary; +} + +function admin_summary_metrics(array $tenants, array $updates): array { $tenantCount = count($tenants); $activeTenants = 0; @@ -40,12 +60,15 @@ function admin_summary_metrics(array $tenants): array $providerCount += (int) ($tenant['provider_count'] ?? 0); } + $updateSummary = admin_update_summary($updates); + return [ ['label' => 'Mandanten gesamt', 'value' => (string) $tenantCount, 'detail' => 'Mandanten im zentralen Portfolio.'], ['label' => 'Aktive Mandanten', 'value' => (string) $activeTenants, 'detail' => 'Bereiche mit aktivem Status.'], ['label' => 'Mitglieder gesamt', 'value' => (string) $memberCount, 'detail' => 'Aktive Zuordnungen über alle Mandanten.'], - ['label' => 'SSO-Provider', 'value' => (string) $providerCount, 'detail' => 'Aktive Identitätsanbieter für die Anmeldung.'], ['label' => 'Tenant-Admins', 'value' => (string) $adminCount, 'detail' => 'Verwaltungszugänge in den Mandanten.'], + ['label' => 'Offene Updates', 'value' => (string) $updateSummary['pending'], 'detail' => 'Noch nicht eingespielte System-Updates.'], + ['label' => 'SSO-Provider', 'value' => (string) $providerCount, 'detail' => 'Aktive Identitätsanbieter für die Anmeldung.'], ]; } @@ -55,6 +78,7 @@ $page = match ($path) { '/admin', '/admin/' => 'overview', '/admin/login' => 'login', '/admin/tenants' => 'tenants', + '/admin/updates' => 'updates', '/admin/logout' => 'logout', default => 'login', }; @@ -67,6 +91,10 @@ $adminLogin = ['state' => app_admin_login_state(), 'message' => null, 'error' => $tenants = []; $editingTenant = null; $summaryMetrics = []; +$licensePlans = []; +$updateItems = []; +$updateSummary = ['pending' => 0, 'success' => 0, 'failed' => 0, 'running' => 0]; +$appVersion = is_file(dirname(__DIR__) . '/version.php') ? (string) require dirname(__DIR__) . '/version.php' : 'unbekannt'; if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && $page === 'logout') { if (hash_equals($_SESSION['admin_csrf'], (string) ($_POST['csrf'] ?? ''))) { @@ -88,7 +116,7 @@ if ($page === 'login' && $pdo instanceof PDO) { $admin = app_admin_user(); } -if (in_array($page, ['overview', 'tenants'], true)) { +if (in_array($page, ['overview', 'tenants', 'updates'], true)) { $admin = app_require_platform_admin(); if (!$pdo instanceof PDO) { @@ -96,25 +124,69 @@ if (in_array($page, ['overview', 'tenants'], true)) { } } -if (in_array($page, ['overview', 'tenants'], true) && $pdo instanceof PDO) { +if (in_array($page, ['overview', 'tenants', 'updates'], true) && $pdo instanceof PDO) { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && !hash_equals($_SESSION['admin_csrf'], (string) ($_POST['csrf'] ?? ''))) { app_flash('Die Sitzung ist abgelaufen. Bitte lade die Seite neu.', 'error'); - app_redirect('/admin/tenants/'); + app_redirect('/admin/' . ($page === 'overview' ? '' : $page . '/')); + } + + if ($page === 'tenants') { + app_handle_platform_tenant_action($pdo); + } + + if ($page === 'updates' && (string) ($_POST['action'] ?? '') === 'run-updates') { + try { + $results = scripts_run_pending_updates( + scripts_update_config_from_env(app_env()), + 'admin:' . (string) ($admin['email'] ?? 'platform') + ); + + $successful = count(array_filter($results, static fn(array $result): bool => ($result['status'] ?? '') === 'success')); + $skipped = count(array_filter($results, static fn(array $result): bool => ($result['status'] ?? '') === 'skipped')); + + app_flash( + $successful . ' Update(s) ausgeführt, ' . $skipped . ' bereits erledigt.', + 'success' + ); + } catch (Throwable $exception) { + app_flash('Die Updates konnten nicht abgeschlossen werden: ' . $exception->getMessage(), 'error'); + } + + app_redirect('/admin/updates/'); } - app_handle_platform_tenant_action($pdo); $tenants = app_admin_tenant_list($pdo); - $summaryMetrics = admin_summary_metrics($tenants); + $licensePlans = app_license_plan_options($pdo); + $updateItems = scripts_list_updates_status(scripts_update_config_from_env(app_env())); + $updateSummary = admin_update_summary($updateItems); + $summaryMetrics = admin_summary_metrics($tenants, $updateItems); if ($page === 'tenants' && isset($_GET['edit']) && $_GET['edit'] !== '') { - $editingTenant = app_query_one( - $pdo, - 'SELECT id, tenant_key, name, status FROM tenants WHERE id = :id LIMIT 1', - ['id' => (string) $_GET['edit']] - ); + $editingTenant = $licensePlans !== [] + ? app_query_one( + $pdo, + <<<'SQL' +SELECT + t.id, + t.tenant_key, + t.name, + t.status, + COALESCE(lp.plan_key, 'team') AS plan_key +FROM tenants t +LEFT JOIN tenant_licenses tl ON tl.tenant_id = t.id AND tl.status = 'active' +LEFT JOIN license_plans lp ON lp.id = tl.license_plan_id +WHERE t.id = :id +LIMIT 1 +SQL, + ['id' => (string) $_GET['edit']] + ) + : app_query_one( + $pdo, + "SELECT id, tenant_key, name, status, 'team' AS plan_key FROM tenants WHERE id = :id LIMIT 1", + ['id' => (string) $_GET['edit']] + ); } } - ?> @@ -123,7 +195,7 @@ if (in_array($page, ['overview', 'tenants'], true) && $pdo instanceof PDO) { Kaffeeliste Admin @@ -131,13 +203,14 @@ if (in_array($page, ['overview', 'tenants'], true) && $pdo instanceof PDO) {
Kaffeeliste Admin -

Zentrale Verwaltung für Mandanten, Zugänge und Betriebsstatus.

+

Zentrale Verwaltung für Mandanten, Lizenzen, Updates und Betriebsstatus.