asdasd
This commit is contained in:
@@ -2,7 +2,7 @@ stages:
|
|||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
BASE_DIRS: "api config data debug modules partials public src tools"
|
BASE_DIRS: "api data debug modules partials public src tools"
|
||||||
CONFIG_BASE_DIR: "config"
|
CONFIG_BASE_DIR: "config"
|
||||||
LOCAL_ROOT: "/mnt/nexusserver"
|
LOCAL_ROOT: "/mnt/nexusserver"
|
||||||
# SITE_DOMAIN_DIR wurde entfernt
|
# SITE_DOMAIN_DIR wurde entfernt
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
const queueBody = document.querySelector('[data-queue-body]');
|
const queueBody = document.querySelector('[data-queue-body]');
|
||||||
const countdownEl = document.querySelector('[data-queue-countdown]');
|
const countdownEl = document.querySelector('[data-queue-countdown]');
|
||||||
const refreshBtn = document.querySelector('[data-queue-refresh]');
|
const refreshBtn = document.querySelector('[data-queue-refresh]');
|
||||||
|
const queueBtn = document.querySelector('[data-queue-button]');
|
||||||
|
const queueCount = document.querySelector('[data-queue-count]');
|
||||||
|
const queueModal = document.querySelector('[data-queue-modal]');
|
||||||
|
const queueClose = document.querySelector('[data-queue-close]');
|
||||||
if (!queueBody || !countdownEl) return;
|
if (!queueBody || !countdownEl) return;
|
||||||
|
|
||||||
let remaining = 10;
|
let remaining = 10;
|
||||||
@@ -179,6 +183,10 @@
|
|||||||
if (data && data.html) {
|
if (data && data.html) {
|
||||||
queueBody.innerHTML = data.html;
|
queueBody.innerHTML = data.html;
|
||||||
}
|
}
|
||||||
|
if (queueCount && typeof data.count === 'number') {
|
||||||
|
queueCount.textContent = String(data.count);
|
||||||
|
queueCount.style.display = data.count > 0 ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
remaining = data && data.next ? data.next : 10;
|
remaining = data && data.next ? data.next : 10;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -195,6 +203,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
setInterval(tick, 1000);
|
setInterval(tick, 1000);
|
||||||
|
fetchQueue();
|
||||||
if (refreshBtn) {
|
if (refreshBtn) {
|
||||||
refreshBtn.addEventListener('click', () => {
|
refreshBtn.addEventListener('click', () => {
|
||||||
fetchQueue();
|
fetchQueue();
|
||||||
@@ -203,6 +212,59 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (queueBtn && queueModal) {
|
||||||
|
queueBtn.addEventListener('click', () => {
|
||||||
|
queueModal.classList.add('is-open');
|
||||||
|
queueModal.setAttribute('aria-hidden', 'false');
|
||||||
|
fetchQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (queueClose && queueModal) {
|
||||||
|
queueClose.addEventListener('click', () => {
|
||||||
|
queueModal.classList.remove('is-open');
|
||||||
|
queueModal.setAttribute('aria-hidden', 'true');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (queueModal) {
|
||||||
|
queueModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === queueModal) {
|
||||||
|
queueModal.classList.remove('is-open');
|
||||||
|
queueModal.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queueBody.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('[data-queue-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
const runId = btn.getAttribute('data-run-id');
|
||||||
|
const action = btn.getAttribute('data-queue-action');
|
||||||
|
if (!runId || !action) return;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('queue_action_json', '1');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.set('run_id', runId);
|
||||||
|
formData.set('action', action);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url.toString(), { method: 'POST', body: formData, cache: 'no-store' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.ok) {
|
||||||
|
if (consoleNotice) {
|
||||||
|
consoleNotice.textContent = data.error || 'Aktion fehlgeschlagen.';
|
||||||
|
consoleNotice.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (consoleNotice) {
|
||||||
|
consoleNotice.textContent = 'Aktion fehlgeschlagen.';
|
||||||
|
consoleNotice.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchQueue();
|
||||||
|
remaining = 10;
|
||||||
|
countdownEl.textContent = String(remaining);
|
||||||
|
});
|
||||||
|
|
||||||
const consoleForm = document.querySelector('[data-console-form]');
|
const consoleForm = document.querySelector('[data-console-form]');
|
||||||
const consoleError = document.querySelector('[data-console-error]');
|
const consoleError = document.querySelector('[data-console-error]');
|
||||||
const consoleNotice = document.querySelector('[data-console-notice]');
|
const consoleNotice = document.querySelector('[data-console-notice]');
|
||||||
|
|||||||
@@ -72,3 +72,55 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: #0b0f17;
|
background: #0b0f17;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queue-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.queue-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ff5a3c;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(10, 14, 24, 0.55);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
.modal.is-open { display: flex; }
|
||||||
|
.modal-card {
|
||||||
|
width: min(1100px, 96vw);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY label
|
|||||||
$renderRuns = function (array $runs): string {
|
$renderRuns = function (array $runs): string {
|
||||||
ob_start();
|
ob_start();
|
||||||
if (!$runs) {
|
if (!$runs) {
|
||||||
echo '<tr><td colspan="7" class="muted">Noch keine Runs.</td></tr>';
|
echo '<tr><td colspan="8" class="muted">Noch keine Runs.</td></tr>';
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
foreach ($runs as $r) {
|
foreach ($runs as $r) {
|
||||||
@@ -49,6 +49,17 @@ $renderRuns = function (array $runs): string {
|
|||||||
echo '<td>' . e((string)($r['created_by'] ?? '')) . '</td>';
|
echo '<td>' . e((string)($r['created_by'] ?? '')) . '</td>';
|
||||||
echo '<td class="muted" style="max-width:240px;"><code>' . e($snippet) . '</code></td>';
|
echo '<td class="muted" style="max-width:240px;"><code>' . e($snippet) . '</code></td>';
|
||||||
echo '<td>' . (!empty($r['timeout_sec']) ? e((string)$r['timeout_sec']) . 's' : 'default') . '</td>';
|
echo '<td>' . (!empty($r['timeout_sec']) ? e((string)$r['timeout_sec']) . 's' : 'default') . '</td>';
|
||||||
|
$status = (string)($r['status'] ?? '');
|
||||||
|
echo '<td>';
|
||||||
|
if ($status === 'queued') {
|
||||||
|
echo '<button class="nav-link" type="button" data-queue-action="cancel" data-run-id="' . e((string)$r['id']) . '">Stoppen</button>';
|
||||||
|
echo '<button class="nav-link" type="button" data-queue-action="delete" data-run-id="' . e((string)$r['id']) . '">Löschen</button>';
|
||||||
|
} elseif ($status === 'running') {
|
||||||
|
echo '<span class="muted">läuft</span>';
|
||||||
|
} else {
|
||||||
|
echo '<button class="nav-link" type="button" data-queue-action="delete" data-run-id="' . e((string)$r['id']) . '">Löschen</button>';
|
||||||
|
}
|
||||||
|
echo '</td>';
|
||||||
echo '</tr>';
|
echo '</tr>';
|
||||||
}
|
}
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
@@ -61,15 +72,75 @@ if (isset($_GET['queue_json'])) {
|
|||||||
LEFT JOIN ' . $table('hosts') . ' h ON h.id = r.host_id
|
LEFT JOIN ' . $table('hosts') . ' h ON h.id = r.host_id
|
||||||
ORDER BY r.id DESC LIMIT 20'
|
ORDER BY r.id DESC LIMIT 20'
|
||||||
)->fetchAll(PDO::FETCH_ASSOC);
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$count = (int)$pdo->query(
|
||||||
|
"SELECT COUNT(*) FROM " . $table('runs') . " WHERE status IN ('queued','running','cancel_requested')"
|
||||||
|
)->fetchColumn();
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'next' => 10,
|
'next' => 10,
|
||||||
|
'count' => $count,
|
||||||
'html' => $renderRuns($runs),
|
'html' => $renderRuns($runs),
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['queue_action_json'])) {
|
||||||
|
$runId = (int)($_POST['run_id'] ?? 0);
|
||||||
|
$action = (string)($_POST['action'] ?? '');
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
if ($runId <= 0 || !in_array($action, ['cancel', 'delete'], true)) {
|
||||||
|
$error = 'Ungültige Anfrage.';
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM ' . $table('runs') . ' WHERE id = :id LIMIT 1');
|
||||||
|
$stmt->execute(['id' => $runId]);
|
||||||
|
$run = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$run) {
|
||||||
|
$error = 'Run nicht gefunden.';
|
||||||
|
} else {
|
||||||
|
$status = (string)($run['status'] ?? '');
|
||||||
|
$driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
$nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')";
|
||||||
|
|
||||||
|
if ($action === 'cancel') {
|
||||||
|
if ($status !== 'queued') {
|
||||||
|
$error = 'Nur queued Runs können gestoppt werden.';
|
||||||
|
} else {
|
||||||
|
$pdo->exec('UPDATE ' . $table('runs') . ' SET status = \'canceled\', finished_at = ' . $nowExpr . ' WHERE id = ' . (int)$runId);
|
||||||
|
try {
|
||||||
|
$redis = module_fn('pi_control', 'redis');
|
||||||
|
$payload = json_encode(['run_id' => $runId]);
|
||||||
|
$redis->command(['LREM', $queueName, '0', $payload]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// ignore redis cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
if ($status === 'running') {
|
||||||
|
$error = 'Laufende Runs können nicht gelöscht werden.';
|
||||||
|
} else {
|
||||||
|
$pdo->prepare('DELETE FROM ' . $table('runs') . ' WHERE id = :id')->execute(['id' => $runId]);
|
||||||
|
try {
|
||||||
|
$redis = module_fn('pi_control', 'redis');
|
||||||
|
$payload = json_encode(['run_id' => $runId]);
|
||||||
|
$redis->command(['LREM', $queueName, '0', $payload]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// ignore redis cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => $error === null,
|
||||||
|
'error' => $error,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_GET['open_console_json'])) {
|
if (isset($_GET['open_console_json'])) {
|
||||||
$hostId = (int)($_POST['terminal_host_id'] ?? 0);
|
$hostId = (int)($_POST['terminal_host_id'] ?? 0);
|
||||||
$presetId = (int)($_POST['terminal_preset_id'] ?? 0);
|
$presetId = (int)($_POST['terminal_preset_id'] ?? 0);
|
||||||
@@ -309,28 +380,42 @@ $runs = $pdo->query(
|
|||||||
<div class="card form-card" style="background:var(--panel-2);">
|
<div class="card form-card" style="background:var(--panel-2);">
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||||
<strong>Queue</strong>
|
<strong>Queue</strong>
|
||||||
<div class="muted" style="display:flex; align-items:center; gap:10px;">
|
<button class="icon-button queue-button" type="button" data-queue-button>
|
||||||
<span>Update in <span data-queue-countdown>10</span>s</span>
|
Queue
|
||||||
<button class="icon-button" type="button" data-queue-refresh title="Jetzt aktualisieren">↻</button>
|
<span class="queue-badge" data-queue-count>0</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:.75rem; overflow:auto;">
|
</div>
|
||||||
<table class="table" style="min-width:560px;">
|
|
||||||
<thead>
|
<div class="modal" data-queue-modal aria-hidden="true">
|
||||||
<tr>
|
<div class="modal-card">
|
||||||
<th>ID</th>
|
<div class="modal-header">
|
||||||
<th>Status</th>
|
<strong>Queue</strong>
|
||||||
<th>Host</th>
|
<div class="modal-actions">
|
||||||
<th>Command</th>
|
<span class="muted">Update in <span data-queue-countdown>10</span>s</span>
|
||||||
<th>Von</th>
|
<button class="icon-button" type="button" data-queue-refresh title="Jetzt aktualisieren">↻</button>
|
||||||
<th>Output</th>
|
<button class="icon-button" type="button" data-queue-close title="Schließen">×</button>
|
||||||
<th>Timeout</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div style="margin-top:.75rem; overflow:auto;">
|
||||||
<tbody data-queue-body>
|
<table class="table" style="min-width:720px;">
|
||||||
<?= $renderRuns($runs) ?>
|
<thead>
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<th>ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Host</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th>Von</th>
|
||||||
|
<th>Output</th>
|
||||||
|
<th>Timeout</th>
|
||||||
|
<th>Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody data-queue-body>
|
||||||
|
<?= $renderRuns($runs) ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user