OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

33 KB
<?php $this->layout('layouts/admin', [
    'page_title' => 'Cronjobs',
    'current_admin_page' => 'cronjobs'
]); ?>

<div class="cronjobs-page">

    <!-- Crontab-Eintrag -->
    <section class="settings-card">
        <div class="settings-card-header">
            <span class="settings-card-icon">&#128268;</span>
            <div>
                <h2><?= $this->te('Crontab-Eintrag') ?></h2>
                <p class="settings-card-desc"><?= $this->t('Eine einzige Zeile in <code>crontab -e</code>. Der Scheduler läuft jede Minute, prüft welche Tasks fällig sind, und stößt sie an. Lange Tasks werden im Hintergrund ausgeführt, damit der Tick nicht blockiert.') ?></p>
            </div>
        </div>
        <div class="settings-card-body">
            <div class="cj-command-row">
                <span class="cj-command-label"><?= $this->te('Scheduler:') ?></span>
                <div style="position:relative;">
                    <code class="cron-command" id="cjMainCmd"><?= $this->e($cron_command) ?></code>
                    <button class="cj-copy-btn" onclick="cjCopyCmd('cjMainCmd', this)" title="<?= $this->te('Kopieren') ?>">&#128203;</button>
                </div>
            </div>
            <div class="cj-migration-notice">
                <?= $this->t('<strong>Migration:</strong> Bisherige Cronjob-Zeilen für <code>send-notifications.php</code>, <code>fetch-tickets.php</code>, <code>process-mail-queue.php</code>, <code>sync-directory.php</code>, <code>relution-sync.php</code> und <code>cve-refresh.php</code> können entfernt werden — der Scheduler ersetzt sie alle. Die alten Skripte bleiben funktionsfähig (z.&nbsp;B. zum Übergang); ein Lock pro Task verhindert Doppel-Läufe falls beide aktiv sind.') ?>
            </div>
            <p style="margin-top:0.75rem;font-size:0.78rem;color:var(--gray-500);">
                <?= $this->t('&#9432; Log-Verzeichnis: <code>{pfad}</code> &mdash; muss für den Cron-User schreibbar sein.', ['pfad' => $this->e($log_dir)]) ?>
            </p>
        </div>
    </section>

    <!-- Webcron (URL-Aufruf) -->
    <section class="settings-card">
        <div class="settings-card-header">
            <span class="settings-card-icon">&#127760;</span>
            <div>
                <h2><?= $this->te('Webcron (URL-Aufruf)') ?></h2>
                <p class="settings-card-desc"><?= $this->te('Alternative für Hoster ohne echten Crontab (z.&nbsp;B. All-Inkl KAS): Der Scheduler-Tick wird per URL-Aufruf angestoßen &mdash; gleiche Wirkung wie der Crontab-Eintrag oben. Der Endpunkt ist mit einem eigenen Token gesichert und standardmäßig deaktiviert.') ?></p>
            </div>
        </div>
        <div class="settings-card-body">
            <form method="post" action="/admin/cronjobs/webcron/settings" style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                <label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;margin:0;">
                    <input type="checkbox" name="webcron_enabled" value="1" <?= !empty($webcron_enabled) ? 'checked' : '' ?>>
                    <span><?= $this->te('Webcron aktivieren') ?></span>
                </label>
                <button type="submit" class="btn btn-sm btn-primary"><?= $this->te('Speichern') ?></button>
                <?php if (!empty($webcron_enabled)): ?>
                    <span class="status-pill status-pill--success"><?= $this->te('Aktiv') ?></span>
                <?php else: ?>
                    <span class="status-pill"><?= $this->te('Deaktiviert') ?></span>
                <?php endif; ?>
            </form>

            <?php if (!empty($webcron_url)): ?>
                <div class="cj-command-row" style="margin-top:0.9rem;">
                    <span class="cj-command-label"><?= $this->te('Cronjob-URL:') ?></span>
                    <div style="position:relative;">
                        <code class="cron-command" id="cjWebcronUrl"><?= $this->e($webcron_url) ?></code>
                        <button class="cj-copy-btn" onclick="cjCopyCmd('cjWebcronUrl', this)" title="<?= $this->te('Kopieren') ?>">&#128203;</button>
                    </div>
                </div>
                <form method="post" action="/admin/cronjobs/webcron/regenerate-token" style="margin-top:0.6rem;"
                      onsubmit="return confirm('Neuen Token erzeugen? Die bisherige Cronjob-URL wird sofort ungültig und muss beim Hoster aktualisiert werden.');">
                    <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                    <button type="submit" class="btn btn-sm"><?= $this->te('Token neu erzeugen') ?></button>
                </form>
            <?php endif; ?>

            <p style="margin-top:0.75rem;font-size:0.78rem;color:var(--gray-500);">
                <?= $this->te('&#9432; Diese URL beim Hoster als Cronjob hinterlegen (bei All-Inkl: KAS &rarr; Tools &rarr; Cronjobs, kleinstes Intervall meist 5 Minuten). Fällige Tasks werden beim nächsten Aufruf nachgeholt. Den Token geheim halten &mdash; wer die URL kennt, kann den Scheduler anstoßen. Bei deaktiviertem Webcron antwortet der Endpunkt mit 403, ohne etwas auszuführen.') ?>
            </p>
        </div>
    </section>

    <!-- Tasks -->
    <section class="settings-card">
        <div class="settings-card-header">
            <span class="settings-card-icon">&#9881;</span>
            <div>
                <h2><?= $this->te('Geplante Tasks') ?></h2>
                <p class="settings-card-desc"><?= $this->te('Intervall, Aktivierung und Hintergrund-Modus pro Task. Änderungen wirken ab dem nächsten Tick.') ?></p>
            </div>
        </div>
        <div class="settings-card-body" style="padding: 0;">
            <?php if (empty($tasks)): ?>
                <div style="padding:2rem;text-align:center;color:var(--gray-400);">
                    <p><?= $this->t('Keine Tasks gefunden — wurde Migration <code>009_cron_tasks.sql</code> bereits ausgeführt?') ?></p>
                </div>
            <?php else:
                // Tasks nach Modul/Themenbereich gruppieren (heuristisch über den Task-Namen).
                $cronRules = [
                    ['isms', 'Informationssicherheit (ISMS)'],
                    ['contract', 'Verträge'],
                    ['sla', 'Tickets'], ['ticket', 'Tickets'], ['mail_queue', 'Tickets'],
                    ['cve', 'CVE & Sicherheit'],
                    ['mail', 'E-Mail'],
                    ['vault', 'Passwort-Tresor'], ['secret', 'Passwort-Tresor'],
                    ['license', 'Lizenzen'],
                    ['loan', 'Geräteausleihe'],
                    ['handout', 'Inventar'], ['asset', 'Inventar'],
                    ['intune', 'Inventar & MDM'], ['relution', 'Inventar & MDM'],
                    ['warranty', 'Inventar & MDM'], ['dell', 'Inventar & MDM'],
                    ['directory', 'Verzeichnis & Sync'], ['entra', 'Verzeichnis & Sync'],
                    ['backup', 'Backup'],
                    ['configlog', 'Konfigurationslog'],
                    ['netdiagram', 'Netzdiagramm'],
                    ['ai_', 'KI'], ['embedding', 'KI'],
                    ['api_auto', 'API-Automatisierung'],
                    ['file', 'Dateien'],
                    ['review', 'Dokumentation'],
                    ['maintenance', 'Wartung'],
                ];
                $grouped = [];
                foreach ($tasks as $t) {
                    $label = 'Sonstige';
                    $name  = strtolower($t['name']);
                    foreach ($cronRules as $rule) {
                        if (str_contains($name, $rule[0])) { $label = $rule[1]; break; }
                    }
                    $grouped[$label][] = $t;
                }
                // Reihenfolge der Regeln; "Sonstige" ans Ende.
                $sorted = [];
                foreach ($cronRules as $rule) {
                    if (isset($grouped[$rule[1]]) && !isset($sorted[$rule[1]])) {
                        $sorted[$rule[1]] = $grouped[$rule[1]];
                    }
                }
                foreach ($grouped as $lbl => $rows) {
                    if (!isset($sorted[$lbl])) { $sorted[$lbl] = $rows; }
                }
                $grouped = $sorted;
            ?>
            <div class="admin-table-wrap">
            <table class="cron-table">
                <thead>
                    <tr>
                        <th scope="col"><?= $this->te('Task') ?></th>
                        <th scope="col"><?= $this->te('Intervall (Min.)') ?></th>
                        <th scope="col"><?= $this->te('Aktiv') ?></th>
                        <th scope="col" title="<?= $this->te('Lange Tasks im Hintergrund — Scheduler-Tick wartet nicht auf Abschluss.') ?>"><?= $this->te('Hintergrund') ?></th>
                        <th scope="col"><?= $this->te('Letzter Lauf') ?></th>
                        <th scope="col"><?= $this->te('Status') ?></th>
                        <th scope="col" class="cj-col-hide-md"><?= $this->te('Nächster Lauf') ?></th>
                        <th scope="col" style="width:130px;"></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($grouped as $groupLabel => $groupTasks): ?>
                    <tr class="cron-group-row">
                        <td colspan="8"><?= $this->e($groupLabel) ?> <span class="cron-group-count"><?= count($groupTasks) ?></span></td>
                    </tr>
                    <?php foreach ($groupTasks as $task):
                        $name        = $task['name'];
                        $lastStatus  = $task['last_status'] ?? 'never';
                        $lastRunAt   = $task['last_run_at'] ?? null;
                        $nextRunAt   = $task['next_run_at'] ?? null;
                        $lastMessage = $task['last_message'] ?? '';
                    ?>
                    <tr id="cjRow-<?= $this->e($name) ?>" data-task="<?= $this->e($name) ?>" class="<?= $task['enabled'] ? '' : 'cj-disabled' ?>">
                        <td>
                            <strong><?= $this->e($task['label']) ?></strong>
                            <small class="cron-desc"><?= $this->e($task['description'] ?? '') ?></small>
                            <small class="cron-name"><code><?= $this->e($name) ?></code></small>
                        </td>
                        <td>
                            <input type="number" min="1" max="43200"
                                   class="cj-input-freq"
                                   value="<?= (int)$task['frequency_minutes'] ?>"
                                   data-task="<?= $this->e($name) ?>" />
                            <small class="cron-freq-label"><?= $this->e($task['frequency_label']) ?></small>
                        </td>
                        <td>
                            <label class="cj-switch">
                                <input type="checkbox" class="cj-toggle-enabled"
                                       data-task="<?= $this->e($name) ?>"
                                       <?= $task['enabled'] ? 'checked' : '' ?> />
                                <span class="cj-switch-slider"></span>
                            </label>
                        </td>
                        <td>
                            <label class="cj-switch">
                                <input type="checkbox" class="cj-toggle-bg"
                                       data-task="<?= $this->e($name) ?>"
                                       <?= $task['run_in_background'] ? 'checked' : '' ?> />
                                <span class="cj-switch-slider"></span>
                            </label>
                        </td>
                        <td>
                            <?php if (!$lastRunAt): ?>
                                <span class="text-muted"><?= $this->te('Noch nie') ?></span>
                            <?php else: ?>
                                <small class="cron-date"><?= date('d.m.Y H:i', strtotime($lastRunAt)) ?></small>
                            <?php endif; ?>
                        </td>
                        <td>
                            <?php if ($lastStatus === 'never'): ?>
                                <span class="cron-status cron-status-unknown">—</span>
                            <?php elseif ($lastStatus === 'success'): ?>
                                <span class="cron-status cron-status-success" title="<?= $this->e($lastMessage) ?>">OK</span>
                            <?php elseif ($lastStatus === 'running'): ?>
                                <span class="cron-status cron-status-running"><?= $this->te('Läuft') ?></span>
                            <?php elseif ($lastStatus === 'skipped'): ?>
                                <span class="cron-status cron-status-unknown"><?= $this->te('Skipped') ?></span>
                            <?php else: ?>
                                <span class="cron-status cron-status-error" title="<?= $this->e($lastMessage) ?>"><?= $this->te('Fehler') ?></span>
                            <?php endif; ?>
                        </td>
                        <td class="cj-col-hide-md">
                            <?php if ($task['enabled'] && $nextRunAt): ?>
                                <small class="cron-date"><?= date('d.m.Y H:i', strtotime($nextRunAt)) ?></small>
                            <?php else: ?>
                                <span class="text-muted">-</span>
                            <?php endif; ?>
                        </td>
                        <td style="white-space:nowrap;">
                            <button class="cj-run-btn cj-run-task" data-task="<?= $this->e($name) ?>" data-label="<?= $this->e($task['label']) ?>">
                                <?= $this->te('&#9654; Jetzt') ?>
                            </button>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                    <?php endforeach; ?>
                </tbody>
            </table>
            </div>
            <?php endif; ?>
        </div>
    </section>

    <!-- Letzte Läufe -->
    <section class="settings-card">
        <div class="settings-card-header">
            <span class="settings-card-icon">&#128203;</span>
            <div style="flex:1;">
                <h2><?= $this->te('Letzte Läufe') ?></h2>
                <p class="settings-card-desc"><?= $this->te('Die letzten 50 protokollierten Cronjob-Läufe (alle Tasks).') ?></p>
            </div>
            <a href="#cjLogSection" class="cj-log-link" onclick="cjShowLog('scheduler');return false;"><?= $this->te('&#128196; scheduler.log') ?></a>
        </div>
        <div class="settings-card-body" style="padding: 0;">
            <?php if (empty($recent_runs)): ?>
                <div style="padding:2rem;text-align:center;color:var(--gray-400);">
                    <p><?= $this->te('Noch keine Läufe protokolliert.') ?></p>
                </div>
            <?php else: ?>
            <div class="admin-table-wrap">
            <table class="cron-table cron-table-compact">
                <thead>
                    <tr>
                        <th scope="col"><?= $this->te('Zeitpunkt') ?></th>
                        <th scope="col"><?= $this->te('Task') ?></th>
                        <th scope="col" class="cj-col-hide-sm"><?= $this->te('Dauer') ?></th>
                        <th scope="col"><?= $this->te('Status') ?></th>
                        <th scope="col" class="cj-col-hide-sm"><?= $this->te('Verarbeitet') ?></th>
                        <th scope="col" class="cj-col-hide-md"><?= $this->te('Meldung') ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($recent_runs as $run):
                        $duration = null;
                        if (!empty($run['finished_at'])) {
                            $duration = strtotime($run['finished_at']) - strtotime($run['started_at']);
                        }
                    ?>
                    <tr>
                        <td><?= date('d.m.Y H:i:s', strtotime($run['started_at'])) ?></td>
                        <td><code style="font-size:0.78rem;"><?= $this->e($run['job_name']) ?></code></td>
                        <td class="cj-col-hide-sm">
                            <?= $duration !== null ? ($duration < 1 ? '< 1s' : $duration . 's') : '<span class="text-muted">läuft</span>' ?>
                        </td>
                        <td>
                            <?php if ($run['status'] === 'success'): ?>
                                <span class="cron-status cron-status-success">OK</span>
                            <?php elseif ($run['status'] === 'running'): ?>
                                <span class="cron-status cron-status-running"><?= $this->te('Läuft') ?></span>
                            <?php else: ?>
                                <span class="cron-status cron-status-error"><?= $this->te('Fehler') ?></span>
                            <?php endif; ?>
                        </td>
                        <td class="cj-col-hide-sm"><?= (int)($run['items_processed'] ?? 0) ?></td>
                        <td class="cj-col-hide-md cron-message"><?= $run['message'] ? $this->e(substr($run['message'], 0, 120)) : '-' ?></td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            </div>
            <?php endif; ?>
        </div>
    </section>

    <!-- Log-Viewer -->
    <section class="settings-card" id="cjLogSection" style="display:none;">
        <div class="settings-card-header">
            <span class="settings-card-icon">&#128196;</span>
            <div style="flex:1;">
                <h2><?= $this->te('Logdatei:') ?> <span id="cjLogTitle"></span></h2>
                <p class="settings-card-desc" id="cjLogMeta"></p>
            </div>
            <div style="display:flex;gap:0.5rem;align-items:center;">
                <button class="cj-run-btn" onclick="cjRefreshLog()" id="cjLogRefreshBtn"><?= $this->te('&#8635; Aktualisieren') ?></button>
                <button aria-label="<?= $this->te('Schließen') ?>" class="cj-run-btn" style="color:var(--gray-500);background:var(--gray-100);border-color:var(--gray-200);" onclick="document.getElementById('cjLogSection').style.display='none'">&#10005;</button>
            </div>
        </div>
        <div class="settings-card-body" style="padding:0;">
            <pre id="cjLogContent" class="cj-log-pre"><?= $this->te('Lade…') ?></pre>
        </div>
    </section>
</div>

<div id="cjToast" class="cj-toast" style="display:none;"></div>

<style>
.cron-command {
    display: block; padding: 0.65rem 2.5rem 0.65rem 1rem;
    background: var(--slate-800); color: var(--slate-200); border-radius: 0.5rem;
    font-size: 0.8rem; font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    overflow-x: auto; white-space: nowrap; user-select: all; margin-top: 0.4rem;
}
.cj-command-row { display: flex; flex-direction: column; gap: 0.25rem; position: relative; }
.cj-command-label { font-size: 0.78rem; font-weight: 600; color: var(--gray-500); }
.cj-copy-btn {
    position: absolute; right: 0.4rem; bottom: 0.4rem;
    background: rgba(255,255,255,0.12); border: none; border-radius: 0.25rem;
    color: var(--slate-400); font-size: 0.85rem; padding: 0.2rem 0.45rem; cursor: pointer;
}
.cj-copy-btn:hover { background: rgba(255,255,255,0.22); color: var(--slate-200); }
.cj-migration-notice {
    margin-top: 0.85rem; padding: 0.7rem 0.95rem;
    background: var(--amber-50); border: 1px solid var(--amber-200); border-radius: 0.4rem;
    font-size: 0.82rem; color: var(--amber-900); line-height: 1.5;
}

.cron-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.cron-table th {
    text-align: left; padding: 0.65rem 1rem;
    background: var(--gray-50); border-bottom: 2px solid var(--gray-200);
    color: var(--gray-500); font-weight: 600; font-size: 0.72rem;
    text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap;
}
.cron-table td { padding: 0.65rem 1rem; border-bottom: 1px solid var(--gray-100); vertical-align: middle; }
.cron-table-compact td { padding: 0.45rem 1rem; font-size: 0.8rem; }
.cron-table tbody tr:last-child td { border-bottom: none; }
.cron-table tbody tr:hover td { background: var(--gray-50); }
.cron-table tbody tr.cron-group-row td {
    background: var(--slate-100); color: var(--slate-700); font-weight: 700;
    font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
    padding: 0.5rem 1rem; border-bottom: 1px solid var(--slate-200);
}
.cron-table tbody tr.cron-group-row:hover td { background: var(--slate-100); }
.cron-group-count {
    display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.45rem;
    background: var(--slate-300); color: var(--slate-800); border-radius: 9999px;
    font-size: 0.7rem; font-weight: 700;
}
.cron-table tbody tr.cj-disabled td { opacity: 0.55; }
.cron-table tbody tr.cj-disabled strong { text-decoration: line-through; }

.cron-desc  { display:block; font-size:0.74rem; color:var(--gray-400); margin-top:0.1rem; }
.cron-name  { display:block; font-size:0.7rem; color:var(--slate-300); margin-top:0.15rem; }
.cron-name code { background: transparent; color: var(--slate-400); padding: 0; }
.cron-date  { display:block; font-size:0.78rem; color:var(--gray-700); }
.cron-freq-label { display:block; font-size:0.68rem; color:var(--gray-400); margin-top:0.15rem; }

.cron-status { font-size:0.68rem; font-weight:700; text-transform:uppercase; padding:0.15rem 0.5rem; border-radius:9999px; white-space:nowrap; }
.cron-status-success  { background:var(--emerald-100); color:var(--emerald-800); }
.cron-status-running  { background:var(--blue-100); color:var(--blue-800); }
.cron-status-error    { background:var(--red-100); color:var(--red-800); }
.cron-status-unknown  { background:var(--gray-100); color:var(--gray-500); }
.cron-message { max-width:240px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--gray-500); }
.text-muted   { color:var(--gray-400); font-size:0.8rem; }

.cj-input-freq {
    width: 80px; padding: 0.3rem 0.45rem; font-size: 0.85rem;
    border: 1px solid var(--gray-300); border-radius: 0.3rem;
    text-align: center;
}
.cj-input-freq:focus { outline: 2px solid var(--blue-300); outline-offset: -1px; border-color: var(--blue-400); }

/* iOS-style switch */
.cj-switch { position: relative; display: inline-block; width: 36px; height: 20px; }
.cj-switch input { opacity: 0; width: 0; height: 0; }
.cj-switch-slider {
    position: absolute; cursor: pointer; inset: 0;
    background: var(--slate-300); transition: 0.18s; border-radius: 9999px;
}
.cj-switch-slider:before {
    position: absolute; content: ""; height: 14px; width: 14px;
    left: 3px; top: 3px; background: var(--color-on-accent); transition: 0.18s; border-radius: 50%;
    box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.cj-switch input:checked + .cj-switch-slider { background: var(--blue-600); }
.cj-switch input:checked + .cj-switch-slider:before { transform: translateX(16px); }

.cj-run-btn {
    display: inline-flex; align-items: center; gap: 0.3rem;
    padding: 0.3rem 0.65rem; font-size: 0.78rem; font-weight: 600;
    color: var(--blue-600); background: var(--blue-50); border: 1px solid var(--blue-200);
    border-radius: 0.375rem; cursor: pointer; transition: all 0.15s; white-space: nowrap;
}
.cj-run-btn:hover  { background: var(--blue-100); border-color: var(--blue-300); }
.cj-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.cj-run-btn.running { color: var(--gray-500); background: var(--gray-100); border-color: var(--gray-200); }
.cj-run-btn.success { color: var(--emerald-800); background: var(--emerald-100); border-color: var(--emerald-300); }
.cj-run-btn.error   { color: var(--red-800); background: var(--red-100); border-color: var(--red-300); }

.cj-toast {
    position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
    padding: 0.75rem 1.25rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500;
    box-shadow: 0 4px 16px rgba(0,0,0,0.15); max-width: 380px;
    animation: cjFadeIn 0.2s ease;
}
.cj-toast.success { background: var(--emerald-100); color: var(--emerald-800); border: 1px solid var(--emerald-300); }
.cj-toast.error   { background: var(--red-100); color: var(--red-800); border: 1px solid var(--red-300); }
@keyframes cjFadeIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }

.cj-log-link {
    display: inline-flex; align-items: center; gap: 0.2rem;
    font-size: 0.78rem; color: var(--gray-500); text-decoration: none;
    padding: 0.3rem 0.55rem; border-radius: 0.3rem; border: 1px solid var(--gray-200);
    transition: all 0.15s;
}
.cj-log-link:hover { background: var(--gray-100); color: var(--gray-700); border-color: var(--gray-300); text-decoration: none; }

.cj-log-pre {
    background: var(--slate-900); color: var(--slate-300);
    font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
    font-size: 0.76rem; line-height: 1.55;
    padding: 1rem 1.25rem; margin: 0;
    max-height: 480px; overflow-y: auto;
    white-space: pre-wrap; word-break: break-all;
    border-radius: 0 0 0.25rem 0.25rem;
}

@media (max-width: 640px) { .cj-col-hide-sm { display: none; } .cron-command { font-size: 0.72rem; } }
@media (max-width: 900px) { .cj-col-hide-md { display: none; } }
</style>

<script>
(function () {
'use strict';

var csrfMeta = document.querySelector('meta[name="csrf-token"]');
function getCsrf() { return csrfMeta ? csrfMeta.content : ''; }

function showToast(msg, type) {
    var t = document.getElementById('cjToast');
    t.textContent = msg;
    t.className = 'cj-toast ' + (type || 'success');
    t.style.display = 'block';
    clearTimeout(t._timeout);
    t._timeout = setTimeout(function() { t.style.display = 'none'; }, 5000);
}

function postUpdate(taskName, payload) {
    var body = new URLSearchParams();
    body.append('csrf_token', getCsrf());
    Object.keys(payload).forEach(function(k) { body.append(k, payload[k]); });

    return fetch('/admin/cronjobs/' + encodeURIComponent(taskName) + '/update', {
        method: 'POST',
        headers: {
            'X-CSRF-Token': getCsrf(),
            'X-Requested-With': 'XMLHttpRequest',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: body.toString()
    }).then(function(r) { return r.json(); });
}

// Frequency input — debounced save on change/blur
document.querySelectorAll('.cj-input-freq').forEach(function(input) {
    var saveTimer;
    function save() {
        var taskName = input.dataset.task;
        var val = parseInt(input.value, 10);
        if (isNaN(val) || val < 1) { val = 1; input.value = 1; }
        if (val > 43200) { val = 43200; input.value = 43200; }
        input.disabled = true;
        postUpdate(taskName, { frequency_minutes: val })
            .then(function(d) {
                input.disabled = false;
                if (d.ok) {
                    showToast('✓ Intervall aktualisiert (' + (d.task ? d.task.frequency_label : val + ' Min.') + ')', 'success');
                    if (d.task && d.task.frequency_label) {
                        var lbl = input.parentElement.querySelector('.cron-freq-label');
                        if (lbl) lbl.textContent = d.task.frequency_label;
                    }
                } else {
                    showToast('✕ ' + (d.error || 'Fehler beim Speichern'), 'error');
                }
            }).catch(function(e) {
                input.disabled = false;
                showToast('Netzwerkfehler: ' + e.message, 'error');
            });
    }
    input.addEventListener('change', save);
    input.addEventListener('blur', function() {
        clearTimeout(saveTimer);
        saveTimer = setTimeout(save, 100);
    });
});

// Enabled toggle
document.querySelectorAll('.cj-toggle-enabled').forEach(function(cb) {
    cb.addEventListener('change', function() {
        var taskName = cb.dataset.task;
        var row = document.getElementById('cjRow-' + taskName);
        cb.disabled = true;
        postUpdate(taskName, { enabled: cb.checked ? '1' : '0' })
            .then(function(d) {
                cb.disabled = false;
                if (d.ok) {
                    if (row) row.classList.toggle('cj-disabled', !cb.checked);
                    showToast('✓ ' + (cb.checked ? 'Aktiviert' : 'Deaktiviert'), 'success');
                } else {
                    cb.checked = !cb.checked;
                    showToast('✕ ' + (d.error || 'Fehler'), 'error');
                }
            }).catch(function(e) {
                cb.disabled = false; cb.checked = !cb.checked;
                showToast('Netzwerkfehler: ' + e.message, 'error');
            });
    });
});

// Background toggle
document.querySelectorAll('.cj-toggle-bg').forEach(function(cb) {
    cb.addEventListener('change', function() {
        var taskName = cb.dataset.task;
        cb.disabled = true;
        postUpdate(taskName, { run_in_background: cb.checked ? '1' : '0' })
            .then(function(d) {
                cb.disabled = false;
                if (d.ok) {
                    showToast('✓ Hintergrund-Modus: ' + (cb.checked ? 'an' : 'aus'), 'success');
                } else {
                    cb.checked = !cb.checked;
                    showToast('✕ ' + (d.error || 'Fehler'), 'error');
                }
            }).catch(function(e) {
                cb.disabled = false; cb.checked = !cb.checked;
                showToast('Netzwerkfehler: ' + e.message, 'error');
            });
    });
});

// "Run now" button
document.querySelectorAll('.cj-run-task').forEach(function(btn) {
    btn.addEventListener('click', function() {
        var taskName = btn.dataset.task;
        var label    = btn.dataset.label;
        btn.disabled = true;
        btn.classList.add('running');
        btn.textContent = '⏳ Läuft…';

        var body = new URLSearchParams();
        body.append('csrf_token', getCsrf());

        fetch('/admin/cronjobs/' + encodeURIComponent(taskName) + '/run', {
            method: 'POST',
            headers: {
                'X-CSRF-Token': getCsrf(),
                'X-Requested-With': 'XMLHttpRequest',
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: body.toString()
        })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            btn.classList.remove('running');
            if (d.ok) {
                btn.classList.add('success');
                btn.textContent = '✓ Erledigt';
                showToast('✓ ' + label + ': ' + (d.message || 'Erfolgreich'), 'success');
                setTimeout(function() { location.reload(); }, 2500);
            } else {
                btn.classList.add('error');
                btn.textContent = '✕ Fehler';
                showToast('✕ ' + label + ': ' + (d.error || 'Unbekannter Fehler'), 'error');
                setTimeout(function() {
                    btn.classList.remove('error');
                    btn.textContent = '▶ Jetzt';
                    btn.disabled = false;
                }, 4000);
            }
        })
        .catch(function(e) {
            btn.classList.remove('running');
            btn.classList.add('error');
            btn.textContent = '✕ Fehler';
            showToast('Netzwerkfehler: ' + e.message, 'error');
            setTimeout(function() {
                btn.classList.remove('error');
                btn.textContent = '▶ Jetzt';
                btn.disabled = false;
            }, 4000);
        });
    });
});

// --- Log viewer (kept from old template, simplified) ---------------------
window.cjCurrentLog = null;
window.cjShowLog = function(name) {
    window.cjCurrentLog = name;
    var section = document.getElementById('cjLogSection');
    var title   = document.getElementById('cjLogTitle');
    var content = document.getElementById('cjLogContent');
    var meta    = document.getElementById('cjLogMeta');
    title.textContent = name + '.log';
    content.textContent = 'Lade…';
    meta.textContent = '';
    section.style.display = '';
    section.scrollIntoView({ behavior: 'smooth', block: 'start' });
    cjLoadLog(name);
};
window.cjRefreshLog = function() { if (window.cjCurrentLog) cjLoadLog(window.cjCurrentLog); };

function cjLoadLog(name) {
    var content = document.getElementById('cjLogContent');
    var meta    = document.getElementById('cjLogMeta');
    var btn     = document.getElementById('cjLogRefreshBtn');
    if (btn) { btn.disabled = true; btn.textContent = '⏳ Lade…'; }

    fetch('/admin/cronjobs/log/' + encodeURIComponent(name), {
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
    })
    .then(function(r) { return r.json(); })
    .then(function(d) {
        if (btn) { btn.disabled = false; btn.textContent = '↻ Aktualisieren'; }
        if (d.ok) {
            content.textContent = d.content;
            content.scrollTop   = content.scrollHeight;
            if (d.mtime) meta.textContent = 'Zuletzt geändert: ' + d.mtime + (d.size ? ' · ' + Math.ceil(d.size / 1024) + ' KB' : '');
        } else {
            content.textContent = 'Fehler: ' + (d.error || 'Unbekannt');
        }
    })
    .catch(function(e) {
        if (btn) { btn.disabled = false; btn.textContent = '↻ Aktualisieren'; }
        content.textContent = 'Netzwerkfehler: ' + e.message;
    });
}

// Copy button for the crontab line
window.cjCopyCmd = function(elementId, btn) {
    var el = document.getElementById(elementId);
    if (!el) return;
    navigator.clipboard.writeText(el.textContent).then(function() {
        if (btn) {
            var orig = btn.textContent;
            btn.textContent = '✓';
            setTimeout(function() { btn.textContent = orig; }, 1500);
        }
    });
};
})();
</script>