OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

settings.php

34 KB
<?php $this->layout('layouts/admin', [
    'page_title'         => 'Monitoring-Einstellungen',
    'current_admin_page' => 'monitoring-settings',
]); ?>

<style>
.mon-admin-tabs { display: flex; gap: .5rem; margin-bottom: 1.5rem; border-bottom: 2px solid var(--gray-200); padding-bottom: 0; }
.mon-admin-tab {
    padding: .55rem 1.2rem; font-size: .875rem; font-weight: 500; border-radius: .4rem .4rem 0 0;
    text-decoration: none; color: var(--gray-500); border: 1px solid transparent; border-bottom: none;
    margin-bottom: -2px; background: transparent; cursor: pointer;
}
.mon-admin-tab:hover { color: var(--blue-700); background: var(--blue-50); }
.mon-admin-tab.active { color: var(--blue-700); background: var(--color-bg); border-color: var(--gray-200); border-bottom: 2px solid var(--color-bg); }

.mon-tab-panel { display: none; }
.mon-tab-panel.active { display: block; }

.mon-code-block {
    background: var(--slate-800); color: var(--slate-200); border-radius: .5rem;
    padding: 1rem 1.25rem; font-family: monospace; font-size: .82rem;
    line-height: 1.6; overflow-x: auto; margin: .75rem 0;
    border: 1px solid var(--slate-700);
}

.token-row { display: flex; gap: .5rem; align-items: center; }
.token-row input { flex: 1; font-family: monospace; font-size: .82rem; }

.mon-howto { border: 1px solid var(--gray-200); border-radius: .45rem; margin-bottom: .6rem; background: var(--color-bg); }
.mon-howto > summary { cursor: pointer; padding: .65rem .9rem; font-size: .9rem; color: var(--gray-800); list-style: none; }
.mon-howto > summary::-webkit-details-marker { display: none; }
.mon-howto > summary::before { content: '▸'; color: var(--gray-400); margin-right: .5rem; }
.mon-howto[open] > summary::before { content: '▾'; }
.mon-howto[open] > summary { border-bottom: 1px solid var(--color-gray-100); }
.mon-howto-body { padding: .5rem .9rem .9rem; }
</style>

<div class="settings-page">
    <?php
        $monToken   = (string)($webhook_token ?? '');
        $monTokenQs = $monToken !== '' ? ('?token=' . rawurlencode($monToken)) : '?token=DEIN_TOKEN';
        // Quellen: Slug, Anzeigename, Kurz-Hinweis
        $monSources = [
            'zabbix'     => ['Zabbix',                       'Webhook-Media-Type (Schritt-für-Schritt unten) – oder API-Polling im 2. Tab.'],
            'prometheus' => ['Prometheus Alertmanager',      'webhook_configs-Receiver mit send_resolved: true.'],
            'grafana'    => ['Grafana Alerting',             'Contact Point „Webhook" (gleiches Format wie Alertmanager).'],
            'uptimekuma' => ['Uptime Kuma',                  'Notification „Webhook", Content-Type application/json.'],
            'checkmk'    => ['Checkmk',                      'Notification-Regel „HTTP/Webhook" – erkennt die Checkmk-Makros direkt.'],
            'icinga'     => ['Icinga 2',                     'Notification-Command mit JSON-Rumpf aus den Icinga-Makros.'],
            'generic'    => ['Generisch (PRTG, eigene Skripte …)', 'Flexible Feldnamen – für alles, was frei konfigurierbares JSON sendet.'],
        ];
    ?>
    <!-- Tabs -->
    <div class="mon-admin-tabs">
        <button type="button" class="mon-admin-tab active" data-tab="webhook"><?= $this->te('&#128279; Webhook (alle Quellen)') ?></button>
        <button type="button" class="mon-admin-tab" data-tab="api"><?= $this->te('&#9881; Zabbix API (Polling)') ?></button>
    </div>

    <!-- ============================================================
         TAB: WEBHOOK (Multi-Source)
         ============================================================ -->
    <div class="mon-tab-panel active" id="tab-webhook">
        <div class="settings-card">
            <div class="settings-card-header">
                <span class="settings-card-icon">&#128279;</span>
                <div>
                    <h2><?= $this->te('Monitoring anbinden (Webhook)') ?></h2>
                    <p class="settings-card-desc"><?= $this->te('Beliebige Monitoring-Systeme senden Alarme per HTTP-POST an OpenNIT – jeweils an die zu ihrer Quelle passende URL. „Resolved/Up"-Meldungen schließen das offene Problem automatisch.') ?></p>
                </div>
            </div>
            <div class="settings-card-body">

                <!-- Geteilter Token -->
                <div class="form-group">
                    <label><?= $this->te('Webhook-Token') ?> <span style="font-weight:400;color:var(--gray-500)"><?= $this->te('(gilt für alle Quellen)') ?></span></label>
                    <div class="token-row">
                        <input type="text" class="form-control" id="tokenInput"
                               value="<?= $this->e($monToken !== '' ? $monToken : '(noch nicht generiert)') ?>" readonly>
                        <button type="button" class="btn btn-secondary btn-sm" onclick="copyText('tokenInput')"><?= $this->te('Kopieren') ?></button>
                        <button type="button" class="btn btn-primary btn-sm" id="regenBtn" onclick="regenToken()"><?= $this->te('Neu generieren') ?></button>
                    </div>
                    <small class="form-hint"><?= $this->t('Pflicht. Wird an jede URL als <code>?token=…</code> angehängt (oder per Header <code>X-Monitoring-Token</code>). Beim Neu-Generieren werden alle bisherigen URLs ungültig.') ?></small>
                </div>

                <!-- Quellen-URLs -->
                <div class="form-group">
                    <label><?= $this->te('Webhook-URL je System') ?></label>
                    <small class="form-hint" style="display:block;margin-bottom:.5rem">
                        <?= $this->t('Wähle dein Monitoring-System und kopiere die fertige URL (Token ist bereits enthalten). Kennt OpenNIT das Format deines Systems nicht, nutze <code>generic</code>.') ?>
                    </small>
                    <div class="table-wrapper">
                        <table class="data-table" style="font-size:.82rem">
                            <thead><tr><th scope="col" style="width:210px"><?= $this->te('System') ?></th><th scope="col"><?= $this->te('Webhook-URL') ?></th><th scope="col" style="width:90px"></th></tr></thead>
                            <tbody>
                                <?php foreach ($monSources as $slug => $info):
                                    $full = $webhook_url . '/' . $slug . $monTokenQs; ?>
                                <tr>
                                    <td style="white-space:nowrap;vertical-align:middle"><strong><?= $this->te($info[0]) ?></strong>
                                        <div style="font-weight:400;color:var(--gray-400);font-size:.74rem"><?= $this->te($info[1]) ?></div>
                                    </td>
                                    <td style="vertical-align:middle"><input type="text" class="form-control form-control-sm" id="surl_<?= $slug ?>" value="<?= $this->e($full) ?>" readonly style="font-family:monospace;font-size:.78rem"></td>
                                    <td style="vertical-align:middle"><button type="button" class="btn btn-secondary btn-sm" onclick="copyText('surl_<?= $slug ?>')"><?= $this->te('Kopieren') ?></button></td>
                                </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                </div>

                <hr style="margin: 1.25rem 0">

                <h3 style="font-size:.97rem;font-weight:700;margin-bottom:.75rem"><?= $this->te('&#128295; Einrichtung je System') ?></h3>
                <p class="form-hint" style="margin-top:0;margin-bottom:1rem"><?= $this->te('Klappe dein System auf. Die URLs oben enthalten den Token bereits.') ?></p>

                <!-- Zabbix -->
                <details class="mon-howto" open>
                    <summary><strong>Zabbix</strong> <?= $this->te('– Webhook-Media-Type') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700);margin:.6rem 0">
                            <?= $this->t('<strong>Der schnelle Weg:</strong> Die Vorlage herunterladen und in Zabbix unter <em>Alerts → Media types → Import</em> einspielen. Adresse und Schlüssel dieser Installation stehen darin schon; danach bleiben nur Schritt 4 und 5.') ?>
                        </p>
                        <p style="margin:0 0 .8rem">
                            <a href="/admin/monitoring/zabbix-template" class="btn btn-primary btn-sm">
                                <i class="fas fa-file-download" aria-hidden="true"></i> <?= $this->te('Zabbix-Vorlage herunterladen') ?>
                            </a>
                            <small style="color:var(--gray-500);margin-left:.5rem"><?= $this->te('Die Datei enthält den Webhook-Schlüssel – wie die Seite hier.') ?></small>
                        </p>
                        <p style="font-size:.85rem;color:var(--gray-700);margin:.6rem 0 .2rem"><?= $this->te('Von Hand geht es so:') ?></p>
                        <ol style="font-size:.85rem;color:var(--gray-700);line-height:1.6;padding-left:1.2rem;margin:.5rem 0">
                            <li><strong><?= $this->te('Media type anlegen:') ?></strong> <em><?= $this->te('Alerts → Media types → Create media type') ?></em> <?= $this->t('(bis Zabbix 6.2: <em>Administration → Media types</em>)') ?><?= $this->t(', Typ <strong>Webhook</strong>, Name z.&nbsp;B. „OpenNIT Monitoring".') ?> <?= $this->te('Der Punkt erscheint nur für Super-Admins.') ?></li>
                            <li><?= $this->t('<strong>Parameter</strong> (Tab <em>Parameters</em>)') ?>:
                                <table style="font-size:.78rem;border-collapse:collapse;width:100%;margin-top:.4rem">
                                    <tr style="background:var(--gray-100)"><th scope="col" style="padding:.3rem .5rem;text-align:left;border:1px solid var(--gray-200)">Name</th><th scope="col" style="padding:.3rem .5rem;text-align:left;border:1px solid var(--gray-200)"><?= $this->te('Value') ?></th></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">webhook_url</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace"><?= $this->e($webhook_url . '/zabbix') ?></td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">webhook_token</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200)"><?= $this->e($monToken !== '' ? $monToken : '(Token oben generieren)') ?></td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">event_id</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{EVENT.ID}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">host</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{HOST.NAME}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">hostname</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{HOST.HOST}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">trigger_name</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{TRIGGER.NAME}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">severity</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{TRIGGER.SEVERITY}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">status</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{EVENT.VALUE}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">message</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{TRIGGER.DESCRIPTION}</td></tr>
                                    <tr><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">url</td><td style="padding:.25rem .5rem;border:1px solid var(--gray-200);font-family:monospace">{TRIGGER.URL}</td></tr>
                                </table>
                                <p style="font-size:.78rem;color:var(--gray-500);margin:.4rem 0 0"><?= $this->t('<code>{EVENT.VALUE}</code> liefert 0 und 1 und ist damit von der Sprache der Zabbix-Oberfläche unabhängig. <code>{EVENT.STATUS}</code> funktioniert ebenso – es wird in beiden Sprachen erkannt.') ?></p>
                            </li>
                            <li><?= $this->t('<strong>Script</strong> (Tab <em>Script</em>)') ?>:
                                <div style="position:relative">
                                    <pre class="mon-code-block" id="zbxScript">var p = JSON.parse(value);

var req = new HttpRequest();
req.addHeader('Content-Type: application/json');

var payload = JSON.stringify({
    event_id:     p.event_id,
    host:         p.host,
    hostname:     p.hostname,
    trigger_name: p.trigger_name,
    severity:     p.severity,
    status:       p.status,
    message:      p.message,
    url:          p.url
});

var response = req.post(
    p.webhook_url + '?token=' + p.webhook_token,
    payload
);

if (req.getStatus() !== 200) {
    throw 'HTTP ' + req.getStatus() + ': ' + response;
}

return 'OK';</pre>
                                    <button type="button" class="btn btn-secondary btn-sm" style="position:absolute;top:.5rem;right:.5rem" onclick="copyPre('zbxScript')"><?= $this->te('Kopieren') ?></button>
                                </div>
                            </li>
                            <li><?= $this->t('<strong>Dem Benutzer zuweisen</strong> (<em>Users → Users → [Benutzer] → Media → Add</em>, bis Zabbix 6.2: <em>Administration → Users</em>): Typ = dieser Media-Type, <em>Send to</em> beliebig ausfüllen. Ohne diesen Schritt läuft die Action ins Leere, weil Zabbix an einen Benutzer sendet, nicht an den Media-Type.') ?></li>
                            <li><?= $this->t('<strong>Action</strong> (<em>Alerts → Actions → Trigger actions</em>, bis Zabbix 6.2: <em>Configuration → Actions</em>) – <em>Create action</em>:') ?>
                                <ul style="margin:.3rem 0 0;padding-left:1.1rem">
                                    <li><?= $this->t('Reiter <em>Action</em>: Name frei wählen, <em>Conditions</em> leer lassen, <em>Enabled</em> angehakt.') ?></li>
                                    <li><?= $this->t('Reiter <em>Operations</em> → <em>Operations</em> → <em>Add</em>: <em>Send message</em>, bei <em>Send to users</em> den Benutzer aus Schritt 4, bei <em>Send only to</em> diesen Media-Type. <em>Steps</em> auf <code>1 - 1</code> lassen, sonst wiederholt sich die Meldung.') ?></li>
                                    <li><?= $this->t('Derselbe Reiter → <em>Recovery operations</em> → <em>Add</em>: <em>Notify all involved</em>. Ohne diesen Teil bleibt eine Störung in OpenNIT für immer offen.') ?></li>
                                </ul>
                                <p style="font-size:.78rem;color:var(--gray-500);margin:.4rem 0 0"><?= $this->t('An den Triggern selbst ist <strong>nichts</strong> einzustellen – eine Action ohne Bedingungen gilt für alle. Einschränken lässt sie sich über ihre <em>Conditions</em>, etwa auf eine Hostgruppe. Eine vorhandene Action lässt sich auch erweitern: dann bekommt OpenNIT dieselben Trigger wie deren bisherige Benachrichtigung.') ?></p>
                            </li>
                            <li><?= $this->t('<strong>Testen:</strong> im Media-Type auf <em>Test</em> – danach erscheint ein Event in der <a href="/monitoring">Monitoring-Übersicht</a>.') ?></li>
                        </ol>
                    </div>
                </details>

                <!-- Prometheus Alertmanager -->
                <details class="mon-howto">
                    <summary><strong><?= $this->te('Prometheus Alertmanager') ?></strong> <?= $this->te('– Webhook-Receiver') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('In der <code>alertmanager.yml</code> einen Receiver auf die <code>prometheus</code>-URL setzen. <code>send_resolved: true</code> schließt behobene Alarme automatisch. Schweregrad aus Label <code>severity</code>, Host aus <code>instance</code>.') ?></p>
                        <pre class="mon-code-block">receivers:
  - name: opennit
    webhook_configs:
      - url: '<?= $this->e($webhook_url . '/prometheus' . $monTokenQs) ?>'
        send_resolved: true
route:
  receiver: opennit</pre>
                    </div>
                </details>

                <!-- Grafana -->
                <details class="mon-howto">
                    <summary><strong><?= $this->te('Grafana Alerting') ?></strong> <?= $this->te('– Contact Point') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('<em>Alerting → Contact points → Add contact point</em>, Typ <strong>Webhook</strong>, URL = die <code>grafana</code>-URL oben. Diesen Contact-Point in der Notification-Policy verwenden. (Gleiches Alerts-Format wie Alertmanager.)') ?></p>
                    </div>
                </details>

                <!-- Uptime Kuma -->
                <details class="mon-howto">
                    <summary><strong><?= $this->te('Uptime Kuma') ?></strong> <?= $this->te('– Notification') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('<em>Settings → Notifications → Setup Notification</em>, Typ <strong>Webhook</strong>, „Post URL" = die <code>uptimekuma</code>-URL oben, „Content Type: application/json". Dem Monitor zuweisen. <strong>DOWN</strong> → Problem, <strong>UP</strong> → automatisch behoben.') ?></p>
                    </div>
                </details>

                <!-- Checkmk -->
                <details class="mon-howto">
                    <summary><strong>Checkmk</strong> <?= $this->te('– Notification-Regel') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('<em>Setup → Notifications → Add rule</em>, Methode <strong>HTTP/Webhook</strong> (oder ein Skript, das die Makros als JSON postet), URL = die <code>checkmk</code>-URL oben. Die Makros werden unverändert erkannt – <strong>RECOVERY</strong> bzw. <code>OK</code>/<code>UP</code> schließt das Problem automatisch.') ?></p>
                        <pre class="mon-code-block">POST <?= $this->e($webhook_url . '/checkmk' . $monTokenQs) ?>

{
  "HOSTNAME": "srv-dc01",
  "SERVICEDESC": "CPU load",
  "SERVICESTATE": "CRIT",
  "SERVICEOUTPUT": "15 min load 12.4 (warn/crit 8.0/10.0)",
  "NOTIFICATIONTYPE": "PROBLEM"
}</pre>
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('Ohne <code>SERVICEDESC</code> gilt die Meldung als Host-Alarm; dann zählen <code>HOSTSTATE</code> und <code>HOSTOUTPUT</code>.') ?></p>
                    </div>
                </details>

                <!-- Icinga -->
                <details class="mon-howto">
                    <summary><strong><?= $this->te('Icinga 2') ?></strong> <?= $this->te('– Notification-Command') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('Ein NotificationCommand anlegen, das die Makros als JSON an die <code>icinga</code>-URL postet, und es der Notification zuweisen. <strong>RECOVERY</strong> bzw. <code>OK</code>/<code>UP</code> schließt das Problem automatisch.') ?></p>
                        <pre class="mon-code-block">POST <?= $this->e($webhook_url . '/icinga' . $monTokenQs) ?>

{
  "host": "$host.name$",
  "service": "$service.name$",
  "state": "$service.state$",
  "output": "$service.output$",
  "type": "$notification.type$"
}</pre>
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('Für Host-Benachrichtigungen <code>service</code> weglassen und <code>$host.state$</code> senden.') ?></p>
                    </div>
                </details>

                <!-- Generisch -->
                <details class="mon-howto">
                    <summary><strong><?= $this->te('PRTG / eigene Skripte') ?></strong> <?= $this->te('– generisch') ?></summary>
                    <div class="mon-howto-body">
                        <p style="font-size:.85rem;color:var(--gray-700)"><?= $this->t('An die <code>generic</code>-URL ein JSON posten. Erkannte Felder (mehrere Schreibweisen erlaubt). Wird <code>event_id</code> mitgegeben, schließt ein späteres <code>"status":"RESOLVED"</code> mit gleicher ID das Problem automatisch.') ?></p>
                        <pre class="mon-code-block">POST <?= $this->e($webhook_url . '/generic' . $monTokenQs) ?>

{
  "host":         "srv01",        // host | hostname | host_name
  "trigger_name": "Dienst down",  // trigger_name | name | subject
  "severity":     "high",         // info|warning|average|high|disaster (+ critical/error/…)
  "status":       "PROBLEM",      // PROBLEM | RESOLVED/OK
  "message":      "Details…",     // message | alert_message | body
  "event_id":     "abc-123"       // optional, zum Auto-Auflösen
}</pre>
                        <p style="font-size:.82rem;color:var(--gray-500)"><?= $this->t('Bei <strong>PRTG</strong> die „Execute HTTP Action"/Notification, bei <strong>Checkmk</strong> die Webhook-Notification mit dieser URL und diesem Body-Template einrichten.') ?></p>
                    </div>
                </details>

                <p class="form-hint" style="margin-top:1rem"><?= $this->te('Hohe/„disaster"-Schweregrade lösen zusätzlich eine In-App-Benachrichtigung aus. Fehlt der Token, antwortet die API mit HTTP&nbsp;401.') ?></p>
            </div>
        </div>
    </div>

    <!-- ============================================================
         TAB: ZABBIX API
         ============================================================ -->
    <div class="mon-tab-panel" id="tab-api">
        <div class="settings-card">
            <div class="settings-card-header">
                <span class="settings-card-icon">&#9881;</span>
                <div>
                    <h2><?= $this->te('Zabbix API (optionales Polling)') ?></h2>
                    <p class="settings-card-desc">
                        <?= $this->te('Alternativ oder ergänzend zum Webhook kann das System die Zabbix-API direkt abfragen. Wird nur benötigt, wenn Zabbix vom Tool aus erreichbar ist.') ?>
                    </p>
                </div>
            </div>
            <div class="settings-card-body">

                <form method="POST" action="/admin/monitoring/settings" class="form">
                    <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">

                    <div class="form-group">
                        <label for="zabbix_api_url"><?= $this->te('Zabbix-URL') ?></label>
                        <input type="url" id="zabbix_api_url" name="zabbix_api_url" class="form-control"
                               value="<?= $this->e($zabbix_api_url ?? '') ?>"
                               placeholder="https://zabbix.example.com">
                        <small class="form-hint"><?= $this->te('Basis-URL der Zabbix-Instanz (ohne /api_jsonrpc.php).') ?></small>
                    </div>

                    <div class="form-group">
                        <label for="zabbix_api_token"><?= $this->te('API-Token') ?></label>
                        <input type="password" id="zabbix_api_token" name="zabbix_api_token" class="form-control"
                               value="<?= $this->e($zabbix_api_token ?? '') ?>"
                               placeholder="<?= $this->te('Zabbix API-Token (empfohlen ab Zabbix 5.4)') ?>">
                        <small class="form-hint"><?= $this->t('Entweder API-Token <em>oder</em> Benutzername + Passwort verwenden.') ?></small>
                    </div>

                    <div class="form-row" style="display:flex;gap:1rem">
                        <div class="form-group" style="flex:1">
                            <label for="zabbix_api_user"><?= $this->te('API-Benutzer') ?></label>
                            <input type="text" id="zabbix_api_user" name="zabbix_api_user" class="form-control"
                                   value="<?= $this->e($zabbix_api_user ?? '') ?>"
                                   placeholder="Admin">
                        </div>
                        <div class="form-group" style="flex:1">
                            <label for="zabbix_api_pass"><?= $this->te('API-Passwort') ?></label>
                            <input type="password" id="zabbix_api_pass" name="zabbix_api_pass" class="form-control"
                                   placeholder="<?= $this->te('Leer lassen = unverändert') ?>">
                            <small class="form-hint"><?= $this->te('Leer lassen, um das gespeicherte Passwort beizubehalten.') ?></small>
                        </div>
                    </div>

                    <div class="form-group">
                        <label for="zabbix_sync_mode"><?= $this->te('Sync-Modus') ?></label>
                        <select id="zabbix_sync_mode" name="zabbix_sync_mode" class="form-control">
                            <option value="webhook" <?= ($zabbix_sync_mode ?? 'webhook') === 'webhook' ? 'selected' : '' ?>>
                                <?= $this->te('Nur Webhook (kein API-Polling)') ?>
                            </option>
                            <option value="api" <?= ($zabbix_sync_mode ?? '') === 'api' ? 'selected' : '' ?>>
                                <?= $this->te('API-Polling (manuell / per Cronjob)') ?>
                            </option>
                        </select>
                    </div>

                    <div class="form-group">
                        <label style="display:inline-flex;align-items:center;gap:.45rem;font-weight:600;">
                            <input type="checkbox" name="mute_during_maintenance" value="1"
                                   <?= !empty($mute_during_maintenance) ? 'checked' : '' ?>
                                   style="width:1rem;height:1rem;">
                            <?= $this->te('W&auml;hrend geplanter Wartungen nicht benachrichtigen') ?>
                        </label>
                        <small class="form-hint">
                            <?= $this->te('Eingehende Meldungen werden weiterhin vollst&auml;ndig aufgezeichnet &ndash; nur der Alarm entf&auml;llt, und der Vorgang ist als stumm gekennzeichnet. Ma&szlig;geblich sind die Wartungen der Statusseite (auf die Minute genau) und Eintr&auml;ge des Wartungskalenders vom Typ &bdquo;Wartung&ldquo; &ndash; letztere kennen nur Tage und decken deshalb den gesamten Tag ab.') ?>
                        </small>
                        <?php if (!empty($wartung_aktiv)): ?>
                        <div class="alert alert-info" style="margin-top:.6rem;">
                            <?= $this->te('Gerade l&auml;uft eine Wartung:') ?> <strong><?= $this->e($wartung_aktiv['titel']) ?></strong>
                            (<?= $this->e($wartung_aktiv['quelle']) ?>).
                            <?= $this->te(!empty($mute_during_maintenance) ? 'Meldungen werden derzeit stumm aufgezeichnet.' : 'Der Schalter ist aus &ndash; es wird normal benachrichtigt.') ?>
                        </div>
                        <?php endif; ?>
                    </div>

                    <div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
                        <button type="submit" class="btn btn-primary"><?= $this->te('&#128190; Speichern') ?></button>
                        <?php if (!empty($zabbix_api_url)): ?>
                        <button type="button" class="btn btn-secondary" id="syncBtn" onclick="syncNow()">
                            <?= $this->te('&#8635; Jetzt synchronisieren') ?>
                        </button>
                        <span id="syncStatus" style="font-size:.82rem;color:var(--gray-500)"></span>
                        <?php endif; ?>
                    </div>

                    <?php if ($zabbix_last_sync): ?>
                    <p style="margin-top:.75rem;font-size:.82rem;color:var(--gray-500)">
                        <?= $this->t('Letzter Sync') ?>: <?= $this->e($zabbix_last_sync) ?>
                    </p>
                    <?php endif; ?>
                </form>
            </div>
        </div>
    </div>
</div>

<div class="card" style="margin-top:1.25rem">
    <div class="card-header">
        <h3 class="card-title"><i class="fas fa-tachometer-alt" aria-hidden="true"></i> <?= $this->te('Kennzahlen abfragen lassen') ?></h3>
    </div>
    <div class="card-body">
        <p class="form-hint" style="margin-top:0">
            <?= $this->te('Die Gegenrichtung zum Webhook oben: Hier holt sich das Überwachungssystem Zahlen aus OpenNIT – Tickets nach Zustand, Planer, Mailabruf und -ausgang, Sicherung, Audit-Kette, freier Platz, Fehlanmeldungen und was demnächst abläuft. Ein Abruf liefert alles auf einmal.') ?>
        </p>

        <p style="margin:.8rem 0 .4rem">
            <a href="/admin/monitoring/zabbix-host-template" class="btn btn-primary btn-sm">
                <i class="fas fa-file-download" aria-hidden="true"></i> <?= $this->te('Zabbix-Vorlage herunterladen') ?>
            </a>
            <small style="color:var(--gray-500);margin-left:.5rem"><?= $this->te('Ohne Token – die Datei enthält kein Geheimnis.') ?></small>
        </p>

        <ol style="font-size:.85rem;color:var(--gray-700);line-height:1.7;padding-left:1.2rem;margin:.8rem 0">
            <li><?= $this->t('In OpenNIT unter <em>Administration → API</em> einen Token mit dem Bereich <strong>„Kennzahlen für die Überwachung"</strong> anlegen.') ?></li>
            <li><?= $this->t('In Zabbix unter <em>Data collection → Templates → Import</em> die Vorlage einspielen.') ?></li>
            <li><?= $this->t('Die Vorlage <strong>OpenNIT by HTTP</strong> an einen Host hängen.') ?></li>
            <li><?= $this->t('Am Host die Makros setzen: <code>{$OPENNIT.URL}</code> ist bereits vorbelegt, <code>{$OPENNIT.TOKEN}</code> trägt den Token aus Schritt 1.') ?></li>
        </ol>

        <p style="font-size:.85rem;color:var(--gray-700);margin:.6rem 0">
            <?= $this->t('Enthalten sind <strong>37 Elemente</strong>, eine Erkennung der geplanten Aufgaben, <strong>12 Auslöser</strong> mit einstellbaren Schwellwerten und ein <strong>Dashboard</strong> mit den fünf Ticketzahlen. Der Endpunkt lautet <code>/api/ex/monitoring/metrics</code> und gibt nur Zähler und Zustände heraus – keine Namen, keine Adressen, nichts je Kunde.') ?>
        </p>
    </div>
</div>

<script>
// Tab-Umschaltung
document.querySelectorAll('.mon-admin-tab').forEach(btn => {
    btn.addEventListener('click', () => {
        document.querySelectorAll('.mon-admin-tab').forEach(b => b.classList.remove('active'));
        document.querySelectorAll('.mon-tab-panel').forEach(p => p.classList.remove('active'));
        btn.classList.add('active');
        document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
    });
});

function copyPre(id) {
    const el = document.getElementById(id);
    navigator.clipboard.writeText(el.textContent).then(() => {
        const btn = el.nextElementSibling;
        const orig = btn.textContent;
        btn.textContent = 'Kopiert!';
        setTimeout(() => btn.textContent = orig, 1500);
    });
}

function copyText(id) {
    const el = document.getElementById(id);
    navigator.clipboard.writeText(el.value).then(() => {
        const btn = el.nextElementSibling;
        const orig = btn.textContent;
        btn.textContent = 'Kopiert!';
        setTimeout(() => btn.textContent = orig, 1500);
    });
}

function regenToken() {
    if (!confirm('Token neu generieren? Der alte Token wird ungültig.')) return;
    const btn = document.getElementById('regenBtn');
    btn.disabled = true;
    const csrf = document.querySelector('meta[name="csrf-token"]').content;
    fetch('/admin/monitoring/token/regenerate', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: '_csrf=' + encodeURIComponent(csrf)
    }).then(r => r.json()).then(d => {
        if (d.ok) {
            // Seite neu laden, damit alle quellen-spezifischen URLs den neuen Token enthalten.
            location.reload();
        } else {
            alert('Fehler beim Generieren.');
            btn.disabled = false;
        }
    }).catch(() => { alert('Netzwerkfehler.'); btn.disabled = false; });
}

function syncNow() {
    const btn = document.getElementById('syncBtn');
    const status = document.getElementById('syncStatus');
    btn.disabled = true;
    status.textContent = 'Synchronisiere…';
    const csrf = document.querySelector('meta[name="csrf-token"]').content;
    fetch('/admin/monitoring/sync', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: '_csrf=' + encodeURIComponent(csrf)
    }).then(r => r.json()).then(d => {
        if (d.ok) {
            status.textContent = 'Fertig: ' + (d.created ?? 0) + ' neu, ' + (d.resolved ?? 0) + ' aufgelöst.';
            status.style.color = '#059669';
        } else {
            status.textContent = 'Fehler: ' + (d.error ?? 'Unbekannt');
            status.style.color = '#dc2626';
        }
        btn.disabled = false;
    }).catch(() => { status.textContent = 'Netzwerkfehler.'; btn.disabled = false; });
}
</script>