Upload via GUI (338 Dateien)
This commit is contained in:
@@ -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
@@ -19,23 +19,71 @@ if (!status_page_enabled()) {
|
||||
$states = status_component_states();
|
||||
$overall = status_overall($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);
|
||||
$open = 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();
|
||||
|
||||
// Aktualisiert sich selbst, damit eine offene Seite nicht veraltet
|
||||
header('Cache-Control: no-cache');
|
||||
header('Refresh: 120');
|
||||
if (($_GET['live'] ?? '') === '1') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
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';
|
||||
?>
|
||||
<div style="max-width:820px;margin:0 auto">
|
||||
<h1><?= e($pageTitle) ?></h1>
|
||||
<div 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>
|
||||
<div class="muted" style="font-size:.82rem;margin-top:2px">Stand: <?= e(date('d.m.Y H:i')) ?> Uhr</div>
|
||||
<div id="status-live-overall" class="card" style="padding:18px 22px;margin:16px 0;border-left:6px solid <?= e($overall['color']) ?>">
|
||||
<strong id="status-live-overall-label" style="font-size:1.2rem;color:<?= e($overall['color']) ?>"><?= e($overall['label']) ?></strong>
|
||||
<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 · Live-Aktualisierung aktiv</div>
|
||||
</div>
|
||||
|
||||
<div id="status-live-incidents">
|
||||
<?php foreach ($open as $i): ?>
|
||||
<div class="card" style="padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626">
|
||||
<strong><?= e($i['title']) ?></strong>
|
||||
@@ -47,17 +95,42 @@ require __DIR__ . '/includes/header_customer.php';
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?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>
|
||||
|
||||
<?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ü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ü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ü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ü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>
|
||||
<?php if (!$past): ?><p class="muted">Keine Störungen in den letzten 14 Tagen.</p><?php endif; ?>
|
||||
<?php foreach ($past as $i): ?>
|
||||
@@ -69,4 +142,92 @@ require __DIR__ . '/includes/header_customer.php';
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</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'; ?>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
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');
|
||||
|
||||
// Kein eingebautes Zugangstoken: Das Update liest die öffentlichen Releases. Ist das Repository privat, kann ein Token
|
||||
|
||||
Reference in New Issue
Block a user