Upload via GUI (338 Dateien)

This commit is contained in:
2026-10-25 17:47:11 +00:00
parent 35809532cf
commit 3aed0cc517
4 changed files with 244 additions and 16 deletions
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* Kleiner, öffentlicher Erreichbarkeitstest für die DeskHub-API.
* Gibt keine internen Details oder Zugangsdaten zurück.
*/
require_once __DIR__ . '/../../../includes/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
try {
get_db()->query('SELECT 1');
echo json_encode(['status' => 'ok'], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(503);
echo json_encode(['status' => 'unavailable'], JSON_UNESCAPED_UNICODE);
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/common.php';
/**
* Update-Prüfung der Apps: GET ?platform=android|windows liefert die aktuell veröffentlichte Version,
* mit &download=1 die Installationsdatei. Die Quelle ist die Plugin-Webseite (Zugangsschlüssel bleibt auf dem Server).
*/
api_require_method('GET');
api_require_staff();
$platform = (string) ($_GET['platform'] ?? '');
if (!in_array($platform, ['android', 'windows'], true)) api_error(400, 'bad_platform', 'Unbekannte Plattform.');
$info = app_publisher_info($platform);
if (!$info) api_json(['available' => false, 'platform' => $platform]);
if (($_GET['download'] ?? '') !== '1') {
api_json([
'available' => true,
'platform' => $platform,
'version' => (string) $info['version'],
'sha256' => (string) $info['sha256'],
'size' => (int) $info['size'],
'notes' => (string) ($info['notes'] ?? ''),
'publishedAt' => (string) ($info['modified_at'] ?? ''),
]);
}
@set_time_limit(0);
while (ob_get_level() > 0) { @ob_end_clean(); }
header('Content-Type: ' . $info['mime']);
header('Content-Disposition: attachment; filename="' . $info['filename'] . '"');
if ((int) $info['size'] > 0) header('Content-Length: ' . (int) $info['size']);
header('X-Content-Type-Options: nosniff');
header('X-DeskHub-SHA256: ' . $info['sha256']);
header('Cache-Control: private, no-store');
$url = app_publisher_endpoint($platform, true);
$headers = function_exists('catalog_access_headers') ? catalog_access_headers($url) : [];
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 0, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => false, CURLOPT_WRITEFUNCTION => static function ($curl, $chunk) { echo $chunk; return strlen($chunk); }]);
curl_exec($ch);
curl_close($ch);
} else {
$remote = @fopen($url, 'rb', false, stream_context_create(['http' => ['timeout' => 0, 'header' => implode("\r\n", $headers)]]));
if ($remote) { fpassthru($remote); fclose($remote); }
}
+176 -15
View File
@@ -19,23 +19,71 @@ if (!status_page_enabled()) {
$states = status_component_states(); $states = status_component_states();
$overall = status_overall($pdo); $overall = status_overall($pdo);
$components = status_components($pdo); $components = status_components($pdo);
foreach ($components as &$component) {
$component['timeline'] = status_component_timeline($pdo, (int) $component['id'], 30);
}
unset($component);
$incidents = status_incidents($pdo, 14); $incidents = status_incidents($pdo, 14);
$open = array_values(array_filter($incidents, fn($i) => $i['status'] !== 'resolved')); $open = array_values(array_filter($incidents, fn($i) => $i['status'] !== 'resolved'));
$past = array_values(array_filter($incidents, fn($i) => $i['status'] === 'resolved')); $past = array_values(array_filter($incidents, fn($i) => $i['status'] === 'resolved'));
$incStates = status_incident_states(); $incStates = status_incident_states();
// Aktualisiert sich selbst, damit eine offene Seite nicht veraltet if (($_GET['live'] ?? '') === '1') {
header('Cache-Control: no-cache'); header('Content-Type: application/json; charset=utf-8');
header('Refresh: 120'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
$liveComponents = [];
foreach ($components as $component) {
$componentState = $states[$component['status']] ?? $states['operational'];
$liveComponents[] = [
'id' => (int) $component['id'],
'status' => (string) $component['status'],
'statusLabel' => (string) $componentState[0],
'color' => (string) $componentState[1],
'uptime' => $component['timeline']['uptime'],
'bars' => array_map(static function (array $bar) use ($states): array {
$key = $bar['status'];
return [
'date' => (string) $bar['date'],
'status' => $key,
'label' => $key !== null ? (string) ($states[$key][0] ?? $key) : 'Keine Daten',
'color' => $key !== null ? (string) ($states[$key][1] ?? '#cbd5e1') : '#dfe4e8',
];
}, $component['timeline']['bars']),
];
}
$liveIncidents = [];
foreach ($open as $incident) {
$liveIncidents[] = [
'title' => (string) $incident['title'],
'statusLabel' => (string) ($incStates[$incident['status']] ?? $incident['status']),
'componentName' => (string) ($incident['component_name'] ?? ''),
'updates' => array_map(static fn(array $update): array => [
'statusLabel' => (string) ($incStates[$update['status']] ?? $update['status']),
'createdAt' => format_dt((string) $update['created_at']),
'body' => (string) ($update['body'] ?? ''),
], $incident['updates']),
];
}
echo json_encode([
'updatedAt' => date('d.m.Y H:i:s'),
'overall' => $overall,
'components' => $liveComponents,
'incidents' => $liveIncidents,
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
exit;
}
header('Cache-Control: no-cache, no-store, must-revalidate');
require __DIR__ . '/includes/header_customer.php'; require __DIR__ . '/includes/header_customer.php';
?> ?>
<div style="max-width:820px;margin:0 auto"> <div style="max-width:820px;margin:0 auto">
<h1><?= e($pageTitle) ?></h1> <h1><?= e($pageTitle) ?></h1>
<div class="card" style="padding:18px 22px;margin:16px 0;border-left:6px solid <?= e($overall['color']) ?>"> <div id="status-live-overall" class="card" style="padding:18px 22px;margin:16px 0;border-left:6px solid <?= e($overall['color']) ?>">
<strong style="font-size:1.2rem;color:<?= e($overall['color']) ?>"><?= e($overall['label']) ?></strong> <strong id="status-live-overall-label" style="font-size:1.2rem;color:<?= e($overall['color']) ?>"><?= e($overall['label']) ?></strong>
<div class="muted" style="font-size:.82rem;margin-top:2px">Stand: <?= e(date('d.m.Y H:i')) ?> Uhr</div> <div id="status-live-updated-at" class="muted" style="font-size:.82rem;margin-top:2px">Stand: <?= e(date("d.m.Y H:i:s")) ?> Uhr &middot; Live-Aktualisierung aktiv</div>
</div> </div>
<div id="status-live-incidents">
<?php foreach ($open as $i): ?> <?php foreach ($open as $i): ?>
<div class="card" style="padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626"> <div class="card" style="padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626">
<strong><?= e($i['title']) ?></strong> <strong><?= e($i['title']) ?></strong>
@@ -47,17 +95,42 @@ require __DIR__ . '/includes/header_customer.php';
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<div class="card" style="padding:6px 20px;margin:16px 0">
<?php if (!$components): ?><p class="muted" style="padding:12px 0">Es sind noch keine Dienste eingetragen.</p><?php endif; ?>
<?php foreach ($components as $k => $c): $st = $states[$c['status']] ?? $states['operational']; ?>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 0;<?= $k ? 'border-top:1px solid var(--border,#e3e9ee)' : '' ?>">
<div><strong><?= e($c['name']) ?></strong><?php if (!empty($c['description'])): ?><div class="muted" style="font-size:.82rem"><?= e($c['description']) ?></div><?php endif; ?></div>
<span style="color:<?= e($st[1]) ?>;font-weight:700;white-space:nowrap"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:<?= e($st[1]) ?>;margin-right:6px"></span><?= e($st[0]) ?></span>
</div>
<?php endforeach; ?>
</div> </div>
<?php if (!$components): ?>
<div class="card" style="padding:18px 20px;margin:16px 0"><p class="muted" style="margin:0">Es sind noch keine Dienste eingetragen.</p></div>
<?php else: ?>
<?php $groups = []; foreach ($components as $c) $groups[trim((string) ($c['category_name'] ?? '')) ?: 'Weitere Dienste'][] = $c; ?>
<?php foreach ($groups as $category => $group): ?>
<h2 style="font-size:1.05rem;margin:22px 0 8px"><?= e($category) ?></h2>
<section class="card" style="padding:0 20px;margin:0 0 16px">
<?php foreach ($group as $groupIndex => $c): $st = $states[$c['status']] ?? $states['operational']; ?>
<div data-status-component="<?= (int) $c['id'] ?>" style="padding:13px 0;<?= $groupIndex < count($group) - 1 ? 'border-bottom:1px solid var(--border,#e3e9ee)' : '' ?>">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">
<div><strong><?= e($c['name']) ?></strong><?php if (!empty($c['description'])): ?><div class="muted" style="font-size:.82rem"><?= e($c['description']) ?></div><?php endif; ?></div>
<span data-status-label style="color:<?= e($st[1]) ?>;font-weight:700;white-space:nowrap"><span data-status-dot style="display:inline-block;width:9px;height:9px;border-radius:50%;background:<?= e($st[1]) ?>;margin-right:6px"></span><span data-status-text><?= e($st[0]) ?></span></span>
</div>
<?php $timeline = $c['timeline']; ?>
<div style="display:flex;align-items:center;gap:14px;margin-top:12px">
<div data-status-bars role="img" aria-label="<?= e($c['name']) ?>: letzte automatische Pr&uuml;fungen" style="display:flex;flex:1;gap:3px;min-width:0">
<?php foreach ($timeline['bars'] as $bar): $barStatus = $bar['status']; $barColor = $barStatus !== null ? ($states[$barStatus][1] ?? '#cbd5e1') : '#dfe4e8'; $barLabel = $barStatus !== null ? ($states[$barStatus][0] ?? $barStatus) : 'Noch keine Verlaufsdaten'; ?>
<span title="<?= e($bar['date'] . ': ' . $barLabel) ?>" style="display:block;flex:1;min-width:2px;height:22px;border-radius:3px;background:<?= e($barColor) ?>"></span>
<?php endforeach; ?>
</div>
<span data-status-uptime title="Verf&uuml;gbarkeit der letzten 24 Stunden" style="min-width:112px;text-align:right;font-size:.82rem;font-weight:600;color:var(--text,#334155)"><?= $timeline['uptime'] === null ? 'Noch keine Pr&uuml;fungen' : e(number_format((float) $timeline['uptime'], 2, ',', '.') . ' %') ?></span>
</div>
<div class="muted" style="display:flex;justify-content:space-between;font-size:.72rem;margin-top:4px"><span>Letzte Pr&uuml;fungen</span><span>Jetzt</span></div>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:6px;font-size:.7rem;color:var(--muted,#64748b)">
<?php foreach ($states as $stateKey => $stateMeta): ?><span><i style="display:inline-block;width:8px;height:8px;border-radius:2px;background:<?= e($stateMeta[1]) ?>;margin-right:4px"></i><?= e($stateMeta[0]) ?></span><?php endforeach; ?>
<span><i style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#dfe4e8;margin-right:4px"></i>Keine Daten</span>
</div>
</div>
<?php endforeach; ?>
</section>
<?php endforeach; ?>
<?php endif; ?>
<h2 style="margin-top:28px">Vergangene Meldungen (letzte 14 Tage)</h2> <h2 style="margin-top:28px">Vergangene Meldungen (letzte 14 Tage)</h2>
<?php if (!$past): ?><p class="muted">Keine Störungen in den letzten 14 Tagen.</p><?php endif; ?> <?php if (!$past): ?><p class="muted">Keine Störungen in den letzten 14 Tagen.</p><?php endif; ?>
<?php foreach ($past as $i): ?> <?php foreach ($past as $i): ?>
@@ -69,4 +142,92 @@ require __DIR__ . '/includes/header_customer.php';
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<script>
(() => {
const endpoint = new URL(window.location.href);
endpoint.searchParams.set('live', '1');
const overallCard = document.getElementById('status-live-overall');
const overallLabel = document.getElementById('status-live-overall-label');
const updatedAt = document.getElementById('status-live-updated-at');
const incidentList = document.getElementById('status-live-incidents');
let polling = false;
function addText(parent, tag, text, className, style) {
const node = document.createElement(tag);
if (className) node.className = className;
if (style) node.style.cssText = style;
node.textContent = text || '';
parent.appendChild(node);
return node;
}
function renderIncidents(incidents) {
if (!incidentList) return;
incidentList.replaceChildren();
for (const incident of incidents || []) {
const card = document.createElement('div');
card.className = 'card';
card.style.cssText = 'padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626';
addText(card, 'strong', incident.title);
addText(card, 'span', incident.statusLabel, '', 'background:#fef3c7;color:#92400e;border-radius:20px;padding:2px 10px;font-size:.72rem;font-weight:700;margin-left:6px');
if (incident.componentName) addText(card, 'div', 'Betrifft: ' + incident.componentName, 'muted', 'font-size:.82rem');
for (const update of incident.updates || []) {
const line = document.createElement('div');
line.style.cssText = 'margin-top:10px;font-size:.9rem';
addText(line, 'strong', update.statusLabel);
addText(line, 'span', ' - ' + update.createdAt, 'muted', 'font-size:.78rem');
if (update.body) addText(line, 'div', update.body, '', 'white-space:pre-line');
card.appendChild(line);
}
incidentList.appendChild(card);
}
}
async function refreshStatus() {
if (polling || document.visibilityState === 'hidden') return;
polling = true;
try {
const response = await fetch(endpoint, {cache: 'no-store', headers: {'Accept': 'application/json'}});
if (!response.ok) return;
const data = await response.json();
if (overallCard && data.overall) {
overallCard.style.borderLeftColor = data.overall.color;
overallLabel.textContent = data.overall.label;
overallLabel.style.color = data.overall.color;
}
if (updatedAt) updatedAt.textContent = 'Stand: ' + data.updatedAt + ' Uhr - Live-Aktualisierung aktiv';
for (const component of data.components || []) {
const row = document.querySelector('[data-status-component="' + component.id + '"]');
if (!row) continue;
const label = row.querySelector('[data-status-label]');
const text = row.querySelector('[data-status-text]');
const dot = row.querySelector('[data-status-dot]');
if (label) label.style.color = component.color;
if (text) text.textContent = component.statusLabel;
if (dot) dot.style.backgroundColor = component.color;
const bars = row.querySelector('[data-status-bars]');
if (bars) {
bars.replaceChildren();
for (const bar of component.bars || []) {
const block = document.createElement('span');
block.title = (bar.date ? bar.date + ': ' : '') + bar.label;
block.style.cssText = 'display:block;flex:1;min-width:2px;height:22px;border-radius:3px;background:' + bar.color;
bars.appendChild(block);
}
}
const uptime = row.querySelector('[data-status-uptime]');
if (uptime) uptime.textContent = component.uptime === null ? 'Noch keine Pr\u00fcfungen' : Number(component.uptime).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' %';
}
renderIncidents(data.incidents);
} catch (_) {
// Bei einem voruebergehenden Netzfehler bleibt die zuletzt geladene Anzeige sichtbar.
} finally {
polling = false;
}
}
refreshStatus();
window.setInterval(refreshStatus, 15000);
})();
</script>
<?php require __DIR__ . '/includes/footer_customer.php'; ?> <?php require __DIR__ . '/includes/footer_customer.php'; ?>
+1 -1
View File
@@ -1,7 +1,7 @@
<?php <?php
if (defined('DESKHUB_VERSION')) return; if (defined('DESKHUB_VERSION')) return;
define('DESKHUB_VERSION', '1.0.35'); define('DESKHUB_VERSION', '1.0.46');
define('DESKHUB_RELEASE_URL', 'https://git.viper.ipv64.net/M_Viper/deskhub/releases'); define('DESKHUB_RELEASE_URL', 'https://git.viper.ipv64.net/M_Viper/deskhub/releases');
// Kein eingebautes Zugangstoken: Das Update liest die öffentlichen Releases. Ist das Repository privat, kann ein Token // Kein eingebautes Zugangstoken: Das Update liest die öffentlichen Releases. Ist das Repository privat, kann ein Token