Files
desktop/system/apps/admin-apps/page.php
Lars Gebhardt-Kusche 2d46b05b1e
All checks were successful
Deploy / deploy-staging (push) Successful in 29s
Deploy / deploy-production (push) Has been skipped
deploy
2026-06-27 23:59:21 +02:00

636 lines
38 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use App\AdminAppsService;
use ModulesCore\ModuleHttp;
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$projectRoot = dirname(__DIR__, 3);
ModuleHttp::requireDesktopAccess($projectRoot);
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
$currentGroups = array_map(
static fn (string $group): string => strtolower(trim($group, '/')),
array_values(array_map('strval', (array) ($currentUser['groups'] ?? [])))
);
$isPartial = !empty($_GET['partial']);
if (!in_array('administrators', $currentGroups, true)) {
http_response_code(403);
$forbidden = '<div class="window-app-shell aa-shell"><div class="window-app-frame aa-frame"><main class="window-app-main aa-main"><div class="window-app-panel aa-panel"><section class="window-app-card aa-card"><p class="aa-message is-error">Kein Zugriff auf Admin Apps.</p></section></div></main></div></div>';
if ($isPartial) {
echo $forbidden;
return;
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Apps</title>
<link rel="stylesheet" href="/assets/desktop/desktop.css">
<link rel="stylesheet" href="/system-app-assets/index.php?app=admin-apps&path=assets/app.css">
</head>
<body><?= $forbidden ?></body>
</html><?php
return;
}
$csrfToken = (string) ($_SESSION['admin_apps_token'] ?? '');
if ($csrfToken === '') {
$csrfToken = bin2hex(random_bytes(16));
$_SESSION['admin_apps_token'] = $csrfToken;
}
$service = new AdminAppsService($projectRoot);
$messages = [];
$errors = [];
$installForm = [
'target_directory_name' => '',
'required_groups' => [],
'available_without_login' => false,
];
$section = trim((string) ($_GET['section'] ?? $_POST['active_section'] ?? 'overview'));
$allowedSections = ['overview', 'installation', 'widgets', 'integrations'];
if (!in_array($section, $allowedSections, true)) {
$section = 'overview';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$installForm = [
'target_directory_name' => trim((string) ($_POST['target_directory_name'] ?? '')),
'required_groups' => array_values(array_map('strval', (array) ($_POST['required_groups'] ?? []))),
'available_without_login' => !empty($_POST['available_without_login']),
];
$postedToken = (string) ($_POST['csrf_token'] ?? '');
if ($postedToken === '' || !hash_equals($csrfToken, $postedToken)) {
$errors[] = 'Die Formularpruefung ist fehlgeschlagen.';
} else {
$action = (string) ($_POST['action'] ?? '');
try {
if ($action === 'save-gitea') {
$service->saveGiteaConfig([
'base_url' => (string) ($_POST['base_url'] ?? ''),
'token' => (string) ($_POST['token'] ?? ''),
'owner' => (string) ($_POST['owner'] ?? ''),
'repositories' => (string) ($_POST['repositories'] ?? ''),
'poll_idle_seconds' => (string) ($_POST['poll_idle_seconds'] ?? ''),
'poll_running_seconds' => (string) ($_POST['poll_running_seconds'] ?? ''),
'visibility' => (string) ($_POST['visibility'] ?? ''),
]);
$messages[] = 'Gitea-Deploy-Status-Konfiguration gespeichert.';
$section = 'integrations';
} elseif ($action === 'save-app-access') {
$service->saveAppAccess(
(string) ($_POST['app_id'] ?? ''),
array_values(array_map('strval', (array) ($_POST['required_groups'] ?? []))),
!empty($_POST['available_without_login'])
);
$messages[] = 'LDAP-Gruppen fuer die App wurden gespeichert.';
$section = 'overview';
} elseif ($action === 'install-app') {
$result = $service->installUploadedApp(
is_array($_FILES['module_zip'] ?? null) ? $_FILES['module_zip'] : [],
$installForm['target_directory_name'],
$installForm['required_groups'],
$installForm['available_without_login']
);
$messages[] = 'App installiert: ' . (string) ($result['manifest']['title'] ?? $result['module_directory']);
$messages[] = 'Zielverzeichnis: custom/apps/' . (string) $result['module_directory'];
$messages[] = 'App-ID fuer Berechtigungen: ' . (string) $result['app_id'];
$installForm = [
'target_directory_name' => '',
'required_groups' => [],
'available_without_login' => false,
];
$section = 'installation';
}
} catch (\Throwable $exception) {
$errors[] = $exception->getMessage();
if ($action === 'install-app') {
$section = 'installation';
}
}
}
}
$bootstrap = $service->bootstrap(true);
$apps = is_array($bootstrap['apps'] ?? null) ? $bootstrap['apps'] : [];
$widgets = is_array($bootstrap['widgets'] ?? null) ? $bootstrap['widgets'] : [];
$stats = is_array($bootstrap['stats'] ?? null) ? $bootstrap['stats'] : [];
$meta = is_array($bootstrap['meta'] ?? null) ? $bootstrap['meta'] : [];
$availableGroups = is_array($meta['available_groups'] ?? null) ? $meta['available_groups'] : [];
$gitea = is_array($bootstrap['gitea'] ?? null) ? $bootstrap['gitea'] : [];
$giteaConfig = is_array($gitea['config'] ?? null) ? $gitea['config'] : [];
$giteaDiagnostics = is_array($gitea['diagnostics'] ?? null) ? $gitea['diagnostics'] : [];
$h = static fn (?string $value): string => htmlspecialchars($value ?? '', ENT_QUOTES);
$sectionUrl = static fn (string $targetSection): string => '/admin/apps/?section=' . rawurlencode($targetSection);
$sectionMeta = [
'overview' => [
'label' => 'Apps',
'title' => 'Apps und Bereitstellung',
'description' => 'Apps sind allgemeine Anwendungen. Sie koennen als Menue-App, Tray-App, Widget-Quelle oder mit optionalem Desktop-Icon auftreten.',
'nav' => 'Bestand und Installationsweg pruefen.',
],
'installation' => [
'label' => 'Installation',
'title' => 'ZIP-Upload und Installation',
'description' => 'Installierbare Fach-Apps liegen unter custom/apps/. ZIP-Dateien koennen hier geprueft, entpackt und mit LDAP-Gruppen verknuepft werden.',
'nav' => 'ZIP hochladen und nach /custom/apps/ installieren.',
],
'widgets' => [
'label' => 'Widgets',
'title' => 'Widget-Quellen',
'description' => 'Hier sieht man nur, welche Widgets aktuell im System registriert sind, aus welcher App sie kommen und welche Start-App oder Status-API dahinterliegt.',
'nav' => 'Registrierte Widgets und Herkunft pruefen.',
],
'integrations' => [
'label' => 'Integrationen',
'title' => 'Gitea-Deploy-Status',
'description' => 'Erste echte Admin-Einstellung fuer das Deploy-Status-Addon inklusive Environment-Datei und Tray-Sichtbarkeit.',
'nav' => 'Addon-Konfiguration bearbeiten.',
],
];
ob_start();
?>
<div id="admin-apps-app" class="window-app-shell aa-shell">
<div class="window-app-frame aa-frame">
<aside class="window-app-sidebar aa-sidebar">
<div class="window-app-brand aa-brand">
<p class="window-app-kicker aa-kicker">Administration</p>
<h1>Admin Apps</h1>
<p class="window-app-copy aa-copy">Zentrale Verwaltungs-App fuer Desktop-Apps, Widgets und erste Integrations-Einstellungen.</p>
</div>
<div class="window-app-nav-list aa-nav-list">
<?php foreach ($sectionMeta as $sectionId => $metaItem): ?>
<a class="window-app-nav-button aa-nav-button<?= $section === $sectionId ? ' is-active' : '' ?>" href="<?= $h($sectionUrl($sectionId)) ?>">
<strong><?= $h($metaItem['label']) ?></strong>
<span class="window-app-meta aa-meta"><?= $h($metaItem['nav']) ?></span>
</a>
<?php endforeach; ?>
</div>
<section class="window-app-card aa-card">
<strong class="aa-side-title">Schnellstatus</strong>
<div class="aa-side-metric">
<span>Environment</span>
<strong><?= $h((string) ($meta['environment'] ?? 'local')) ?></strong>
</div>
<div class="aa-side-metric">
<span>Apps gesamt</span>
<strong><?= (int) ($stats['apps_total'] ?? 0) ?></strong>
</div>
<div class="aa-side-metric">
<span>Widgets</span>
<strong><?= (int) ($stats['widgets_total'] ?? 0) ?></strong>
</div>
<div class="aa-side-metric">
<span>Admin-only</span>
<strong><?= (int) ($stats['admin_only_apps'] ?? 0) ?></strong>
</div>
</section>
</aside>
<main class="window-app-main aa-main">
<div class="window-app-panel aa-panel">
<section class="window-app-hero aa-hero">
<div>
<p class="window-app-kicker aa-kicker">Admin Registry</p>
<h2 class="window-app-title"><?= $h($sectionMeta[$section]['title']) ?></h2>
<p class="window-app-copy aa-copy"><?= $h($sectionMeta[$section]['description']) ?></p>
</div>
<div class="window-app-pill-row aa-pill-row">
<span class="window-app-pill">Administrators only</span>
<span class="window-app-pill">Desktop Auth</span>
<span class="window-app-pill">Section: <?= $h($sectionMeta[$section]['label']) ?></span>
</div>
</section>
<?php if ($messages !== []): ?>
<section class="window-app-card aa-card">
<div class="aa-message is-success">
<strong>Ergebnis</strong>
<ul class="aa-list">
<?php foreach ($messages as $message): ?>
<li><?= $h($message) ?></li>
<?php endforeach; ?>
</ul>
</div>
</section>
<?php endif; ?>
<?php if ($errors !== []): ?>
<section class="window-app-card aa-card">
<div class="aa-message is-error">
<strong>Bitte pruefen</strong>
<ul class="aa-list">
<?php foreach ($errors as $error): ?>
<li><?= $h($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'overview'): ?>
<section class="aa-grid aa-grid--stats">
<article class="window-app-card aa-card aa-stat-card">
<span>Installierbare Apps</span>
<strong><?= (int) ($stats['installable_apps'] ?? 0) ?></strong>
</article>
<article class="window-app-card aa-card aa-stat-card">
<span>Systemtools</span>
<strong><?= (int) ($stats['system_tools'] ?? 0) ?></strong>
</article>
<article class="window-app-card aa-card aa-stat-card">
<span>Core-Apps</span>
<strong><?= (int) (($stats['apps_total'] ?? 0) - ($stats['installable_apps'] ?? 0) - ($stats['system_tools'] ?? 0)) ?></strong>
</article>
</section>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Registry</p>
<h3 class="aa-section-title">Desktop-Apps</h3>
</div>
</div>
<div class="aa-table-wrap">
<table class="aa-table">
<thead>
<tr>
<th>Titel</th>
<th>Scope</th>
<th>Typ</th>
<th>LDAP-Gruppen</th>
<th>Route</th>
<th>Flags</th>
</tr>
</thead>
<tbody>
<?php foreach ($apps as $app): ?>
<tr>
<td>
<strong><?= $h((string) ($app['title'] ?? '')) ?></strong>
<div class="aa-table-copy"><?= $h((string) ($app['summary'] ?? '')) ?></div>
</td>
<td><?= $h((string) ($app['app_scope'] ?? '')) ?></td>
<td><?= $h(!empty($app['installable']) ? 'Custom App' : 'System') ?></td>
<td>
<div class="aa-inline-access-form">
<p class="aa-inline-access-current"><?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
<button
class="window-app-button aa-inline-access-trigger"
type="button"
data-aa-open-modal="access-<?= $h((string) ($app['app_id'] ?? '')) ?>"
>Berechtigungen</button>
</div>
<dialog class="aa-modal" data-aa-modal="access-<?= $h((string) ($app['app_id'] ?? '')) ?>">
<form class="aa-modal-card" method="post" action="/admin/apps/">
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
<input type="hidden" name="action" value="save-app-access">
<input type="hidden" name="active_section" value="overview">
<input type="hidden" name="app_id" value="<?= $h((string) ($app['app_id'] ?? '')) ?>">
<div class="aa-modal-head">
<div>
<p class="window-app-kicker aa-kicker">Berechtigungen</p>
<h4 class="aa-modal-title"><?= $h((string) ($app['title'] ?? '')) ?></h4>
<p class="aa-copy">LDAP-Gruppen und Freigabe fuer den Logoff-Desktop zentral festlegen.</p>
</div>
<button class="aa-modal-close" type="button" data-aa-close-modal>×</button>
</div>
<div class="aa-modal-body">
<p class="aa-inline-access-current">Aktuell: <?= $h((string) ($app['required_groups_display'] ?? '')) ?></p>
<label class="aa-inline-access-toggle">
<input
type="checkbox"
name="available_without_login"
value="1"
<?= !empty($app['available_without_login']) ? 'checked' : '' ?>
>
<span>Auch ohne Login auf dem Logoff-Desktop anzeigen</span>
</label>
<?php if ($availableGroups !== []): ?>
<div class="aa-inline-access-groups">
<?php foreach ($availableGroups as $group): ?>
<label class="aa-inline-access-option">
<input
type="checkbox"
name="required_groups[]"
value="<?= $h((string) ($group['id'] ?? '')) ?>"
<?= in_array((string) ($group['id'] ?? ''), array_values(array_map('strval', (array) ($app['required_groups'] ?? []))), true) ? 'checked' : '' ?>
>
<span>
<strong><?= $h((string) ($group['label'] ?? '')) ?></strong>
<small><?= $h((string) ($group['id'] ?? '')) ?></small>
</span>
</label>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="aa-table-copy">Keine LDAP-Gruppen gefunden.</p>
<?php endif; ?>
</div>
<div class="aa-actions">
<button class="window-app-button aa-secondary" type="button" data-aa-close-modal>Abbrechen</button>
<button class="window-app-button aa-primary aa-inline-access-submit" type="submit">Speichern</button>
</div>
</form>
</dialog>
</td>
<td><code><?= $h((string) ($app['entry_route'] ?? '')) ?></code></td>
<td>
<div class="aa-flag-list">
<?php if (!empty($app['show_on_desktop'])): ?><span class="window-app-pill">Desktop</span><?php endif; ?>
<?php if (!empty($app['supports_widget'])): ?><span class="window-app-pill">Widget</span><?php endif; ?>
<?php if (!empty($app['supports_tray'])): ?><span class="window-app-pill">Tray</span><?php endif; ?>
<?php if (!empty($app['enabled_by_default'])): ?><span class="window-app-pill">Default</span><?php endif; ?>
<?php if (!empty($app['available_without_login'])): ?><span class="window-app-pill">Logoff Desktop</span><?php endif; ?>
<?php if (!empty($app['admin_only'])): ?><span class="window-app-pill">Admin only</span><?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Installation</p>
<h3 class="aa-section-title">Aktueller Installationsweg</h3>
<p class="aa-copy">Neue Apps koennen als Modul-Verzeichnis unter <code>custom/apps/</code> liegen. Fuer ZIP-Dateien steht der eigentliche Installer im Bereich <strong>Installation</strong> zur Verfuegung.</p>
</div>
</div>
</section>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Flags</p>
<h3 class="aa-section-title">Bedeutung der Flags</h3>
<p class="aa-copy"><strong>Default</strong> bedeutet: Die App wird fuer neue Benutzer oder fuer Benutzer ohne explizite Auswahl initial vorausgewaehlt. <strong>Logoff Desktop</strong> bedeutet: Die App darf bereits ohne Login auf der offenen Desktop-Oberflaeche erscheinen, sofern sie als Desktop-App darstellbar ist.</p>
<p class="aa-copy">Gespeichert wird in <?= $h((string) ($meta['app_access_config_path'] ?? '')) ?>. Dort liegen sowohl LDAP-Gruppen als auch die Freigabe fuer den offenen Desktop.</p>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'installation'): ?>
<section class="aa-grid aa-grid--stats">
<article class="window-app-card aa-card aa-stat-card">
<span>Installierbare Apps</span>
<strong><?= (int) ($stats['installable_apps'] ?? 0) ?></strong>
</article>
<article class="window-app-card aa-card aa-stat-card">
<span>Systemtools</span>
<strong><?= (int) ($stats['system_tools'] ?? 0) ?></strong>
</article>
</section>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">ZIP-Installer</p>
<h3 class="aa-section-title">App als ZIP hochladen</h3>
<p class="aa-copy">Das ZIP muss genau ein Modulverzeichnis enthalten. Geprueft werden oberstes Verzeichnis, Pflichtdateien wie <code>module.json</code> und <code>desktop.php</code>, Name-Konflikte sowie einfache Hochrisiko-Muster im Code.</p>
</div>
</div>
<form class="aa-form" method="post" action="/admin/apps/" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
<input type="hidden" name="action" value="install-app">
<input type="hidden" name="active_section" value="installation">
<div class="aa-form-grid">
<label class="aa-field aa-field--full">
<span>ZIP-Datei</span>
<input type="file" name="module_zip" accept=".zip,application/zip">
</label>
<label class="aa-field">
<span>Zielverzeichnis in <code>custom/apps/</code></span>
<input type="text" name="target_directory_name" value="<?= $h((string) $installForm['target_directory_name']) ?>" placeholder="Optional, sonst Name aus ZIP">
</label>
<label class="aa-field aa-field--full aa-field--check">
<span class="aa-check-row">
<input type="checkbox" name="available_without_login" value="1" <?= !empty($installForm['available_without_login']) ? 'checked' : '' ?>>
<span>App nach der Installation direkt fuer den Logoff-Desktop freigeben</span>
</span>
</label>
</div>
<div class="aa-install-groups">
<strong>LDAP-Gruppen nach der Installation</strong>
<p class="aa-copy">Leere Auswahl bedeutet: alle sichtbaren Benutzer. Die Werte werden sofort in <?= $h((string) ($meta['app_access_config_path'] ?? '')) ?> hinterlegt.</p>
<div class="aa-access-groups">
<?php if ($availableGroups === []): ?>
<p class="aa-copy">Keine LDAP-Gruppen aus der Registration-Konfiguration gefunden.</p>
<?php else: ?>
<?php foreach ($availableGroups as $group): ?>
<label class="window-app-item aa-group-item">
<input
type="checkbox"
name="required_groups[]"
value="<?= $h((string) ($group['id'] ?? '')) ?>"
<?= in_array((string) ($group['id'] ?? ''), array_values(array_map('strval', (array) $installForm['required_groups'])), true) ? 'checked' : '' ?>
>
<span class="window-app-item-main aa-group-main">
<strong><?= $h((string) ($group['label'] ?? '')) ?></strong>
<span class="aa-meta"><?= $h((string) ($group['id'] ?? '')) ?></span>
</span>
</label>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="aa-actions">
<button class="window-app-button aa-primary" type="submit">ZIP pruefen und installieren</button>
</div>
</form>
</section>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Checks</p>
<h3 class="aa-section-title">Was geprueft wird</h3>
<p class="aa-copy">Der Installer erwartet ein einzelnes Modulverzeichnis im ZIP, prueft auf <code>module.json</code> und <code>desktop.php</code>, blockiert Pfad-Traversal, Symlinks und typische Hochrisiko-Funktionen wie <code>exec()</code> oder <code>shell_exec()</code>. Das ist ein Plausibilitaetscheck, keine vollstaendige Sicherheitsgarantie.</p>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'widgets'): ?>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Widget Registry</p>
<h3 class="aa-section-title">Registrierte Widgets</h3>
<p class="aa-copy">Dieser Bereich ist rein technisch gemeint: Er zeigt, welche Widgets derzeit registriert sind, aus welcher Quelle sie kommen und welche App oder API damit verbunden ist. Er ist keine separate Benutzerverwaltung.</p>
</div>
</div>
<div class="aa-table-wrap">
<table class="aa-table">
<thead>
<tr>
<th>Widget</th>
<th>Zone</th>
<th>Quelle</th>
<th>Launch-App</th>
<th>Status-API</th>
</tr>
</thead>
<tbody>
<?php foreach ($widgets as $widget): ?>
<tr>
<td>
<strong><?= $h((string) ($widget['title'] ?? '')) ?></strong>
<div class="aa-table-copy"><?= $h((string) ($widget['summary'] ?? '')) ?></div>
</td>
<td><?= $h((string) ($widget['zone'] ?? 'sidebar')) ?></td>
<td>
<code><?= $h((string) (($widget['source_app_id'] ?? '') !== '' ? $widget['source_app_id'] : 'global')) ?></code>
<?php if (($widget['source_module_id'] ?? '') !== ''): ?>
<div class="aa-table-copy">Modul: <?= $h((string) $widget['source_module_id']) ?></div>
<?php endif; ?>
</td>
<td><code><?= $h((string) ($widget['launch_app_id'] ?? '')) ?></code></td>
<td><code><?= $h((string) ($widget['status_api'] ?? '')) ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'integrations'): ?>
<section class="window-app-card aa-card">
<div class="aa-section-head">
<div>
<p class="window-app-kicker aa-kicker">Addon</p>
<h3 class="aa-section-title">Gitea-Deploy-Status</h3>
<p class="aa-copy">Die Werte werden in der aktuellen Environment-Datei gespeichert und beim Deploy in das gemischte `config/`-Verzeichnis uebernommen.</p>
</div>
</div>
<form class="aa-form aa-form--gitea" method="post" action="/admin/apps/">
<input type="hidden" name="csrf_token" value="<?= $h($csrfToken) ?>">
<input type="hidden" name="action" value="save-gitea">
<input type="hidden" name="active_section" value="integrations">
<div class="aa-form-grid">
<label class="aa-field">
<span>Gitea API Base URL</span>
<input type="text" name="base_url" value="<?= $h((string) ($giteaConfig['base_url'] ?? '')) ?>" placeholder="https://gitea.example.tld/api/v1">
</label>
<label class="aa-field">
<span>Owner oder Organisation</span>
<input type="text" name="owner" value="<?= $h((string) ($giteaConfig['owner'] ?? '')) ?>" placeholder="private">
</label>
<label class="aa-field">
<span>Idle Polling (Sek.)</span>
<input type="number" min="1" name="poll_idle_seconds" value="<?= $h((string) ($giteaConfig['poll_idle_seconds'] ?? '5')) ?>">
</label>
<label class="aa-field">
<span>Running Polling (Sek.)</span>
<input type="number" min="1" name="poll_running_seconds" value="<?= $h((string) ($giteaConfig['poll_running_seconds'] ?? '3')) ?>">
</label>
<label class="aa-field aa-field--full">
<span>Token</span>
<input type="password" name="token" value="<?= $h((string) ($giteaConfig['token'] ?? '')) ?>" placeholder="Persoenlicher API-Token">
</label>
<label class="aa-field aa-field--full">
<span>Repositories</span>
<textarea name="repositories" rows="5" placeholder="owner/repo&#10;owner/zweites-repo"><?= $h(implode("\n", array_values(array_map('strval', (array) ($giteaConfig['repositories'] ?? []))))) ?></textarea>
</label>
<label class="aa-field">
<span>Tray-Sichtbarkeit</span>
<select name="visibility">
<?php foreach (['admins' => 'Nur Admins', 'all' => 'Alle Benutzer', 'disabled' => 'Deaktiviert'] as $value => $label): ?>
<option value="<?= $h($value) ?>"<?= (string) ($giteaConfig['visibility'] ?? 'admins') === $value ? ' selected' : '' ?>><?= $h($label) ?></option>
<?php endforeach; ?>
</select>
</label>
<div class="window-app-card aa-card aa-inline-card">
<strong>Aktuelle Datei</strong>
<code><?= $h((string) ($meta['gitea_config_path'] ?? '')) ?></code>
<p>Der Tray ist fuer den aktuellen Benutzer <?= !empty($gitea['tray_visible_for_current_user']) ? 'sichtbar' : 'nicht sichtbar' ?>.</p>
</div>
</div>
<div class="aa-actions">
<button class="window-app-nav-button aa-primary" type="submit">Gitea-Einstellungen speichern</button>
</div>
</form>
</section>
<section class="aa-grid aa-grid--diag">
<article class="window-app-card aa-card aa-diag-card">
<span>Konfiguriert</span>
<strong><?= !empty($giteaDiagnostics['configured']) ? 'Ja' : 'Nein' ?></strong>
</article>
<article class="window-app-card aa-card aa-diag-card">
<span>Token vorhanden</span>
<strong><?= !empty($giteaDiagnostics['token_present']) ? 'Ja' : 'Nein' ?></strong>
</article>
<article class="window-app-card aa-card aa-diag-card">
<span>Repos konfiguriert</span>
<strong><?= count((array) ($giteaDiagnostics['repositories_configured'] ?? [])) ?></strong>
</article>
<article class="window-app-card aa-card aa-diag-card">
<span>Visibility</span>
<strong><?= $h((string) ($giteaDiagnostics['visibility'] ?? 'admins')) ?></strong>
</article>
</section>
<?php endif; ?>
</div>
</main>
</div>
</div>
<?php
$content = ob_get_clean();
if ($isPartial) {
echo $content;
return;
}
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Apps</title>
<link rel="stylesheet" href="/assets/desktop/desktop.css">
<link rel="stylesheet" href="/system-app-assets/index.php?app=admin-apps&path=assets/app.css">
</head>
<body style="margin:0;background:#d7dde5;">
<?= $content ?>
<script src="/system-app-assets/index.php?app=admin-apps&path=assets/app.js"></script>
<script>
window.initAdminAppsApp?.(document.getElementById('admin-apps-app'), { standalone: true });
</script>
</body>
</html>