OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

class-admin-settings.php

39 KB
<?php

if (!defined('ABSPATH')) {
    exit;
}

/**
 * WordPress Admin Einstellungsseite
 *
 * Einstellungen unter: WordPress Admin → Einstellungen → OpenNIT
 */
class OpenNIT_AdminSettings {

    public function register(): void {
        add_action('admin_menu',    [$this, 'add_settings_page']);
        add_action('admin_init',    [$this, 'register_settings']);
        add_action('admin_init',    [$this, 'maybe_sync_menu_name']);
        add_action('admin_notices', [$this, 'show_notices']);
        add_action('wp_ajax_opennit_test_connection',  [$this, 'ajax_test_connection']);
        add_action('wp_ajax_opennit_flush_wiki_cache', [$this, 'ajax_flush_wiki_cache']);
        add_action('wp_ajax_opennit_diagnose',         [$this, 'ajax_diagnose']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
    }

    public function add_settings_page(): void {
        $menu_name = (string)get_option('opennit_menu_name', 'OpenNIT | Connector');
        add_menu_page(
            $menu_name,
            $menu_name,
            'manage_options',
            'opennit-connector',
            [$this, 'render_page'],
            'dashicons-networking',
            30
        );
    }

    public function register_settings(): void {
        register_setting('opennit_settings', 'opennit_api_url', [
            'sanitize_callback' => 'esc_url_raw',
            'default'           => '',
        ]);
        register_setting('opennit_settings', 'opennit_api_token', [
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ]);
        register_setting('opennit_settings', 'opennit_api_signing_secret', [
            'sanitize_callback' => 'sanitize_text_field',
            'default'           => '',
        ]);
        register_setting('opennit_settings', 'opennit_ticket_page_url', [
            'sanitize_callback' => 'esc_url_raw',
            'default'           => '',
        ]);

        // Hinweis: Die KI-Assistent-Einstellungen (Bubble, Titel, Begrüßung,
        // Kontext) werden zentral im OpenNIT-Backend unter Admin → API gepflegt
        // und vom Plugin automatisch übernommen – daher hier keine Optionen mehr.
    }

    /**
     * Menü-Namen einmalig pro Stunde vom API laden (nur im Admin-Kontext).
     */
    public function maybe_sync_menu_name(): void {
        if (get_transient('opennit_menu_name_synced')) {
            return;
        }
        $client = new OpenNIT_ApiClient();
        $config = $client->get_config();
        if (!is_wp_error($config) && !empty($config['menu_name'])) {
            update_option('opennit_menu_name', sanitize_text_field($config['menu_name']));
        }
        set_transient('opennit_menu_name_synced', 1, HOUR_IN_SECONDS);
    }

    public function render_page(): void {
        if (!current_user_can('manage_options')) {
            return;
        }
        $api_url = get_option('opennit_api_url', '');
        $connected = !empty($api_url) && !empty(get_option('opennit_api_token', ''));
        ?>
        <div class="wrap">

            <style>
                .opennit-header {
                    display: flex;
                    align-items: center;
                    gap: 14px;
                    margin: 24px 0 28px;
                }
                .opennit-header-logo {
                    width: 42px;
                    height: 42px;
                    background: #1d4ed8;
                    border-radius: 10px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    flex-shrink: 0;
                }
                .opennit-header-logo svg {
                    width: 24px;
                    height: 24px;
                    fill: #fff;
                }
                .opennit-header h1 {
                    margin: 0;
                    padding: 0;
                    font-size: 22px;
                    font-weight: 700;
                    color: #111827;
                    line-height: 1.2;
                }
                .opennit-header p {
                    margin: 3px 0 0;
                    color: #6b7280;
                    font-size: 13px;
                }
                .opennit-card {
                    background: #fff;
                    border: 1px solid #e5e7eb;
                    border-radius: 10px;
                    padding: 24px 28px;
                    margin-bottom: 20px;
                }
                .opennit-card h2 {
                    font-size: 15px;
                    font-weight: 700;
                    color: #111827;
                    margin: 0 0 18px;
                    padding: 0 0 12px;
                    border-bottom: 1px solid #f3f4f6;
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }
                .opennit-card h2 .card-icon {
                    width: 20px;
                    height: 20px;
                    background: #eff6ff;
                    border-radius: 5px;
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                    font-size: 11px;
                }
                .opennit-steps {
                    counter-reset: steps;
                    list-style: none;
                    margin: 0;
                    padding: 0;
                    display: flex;
                    flex-direction: column;
                    gap: 14px;
                }
                .opennit-steps li {
                    counter-increment: steps;
                    display: flex;
                    align-items: flex-start;
                    gap: 14px;
                    font-size: 13.5px;
                    color: #374151;
                    line-height: 1.5;
                }
                .opennit-steps li::before {
                    content: counter(steps);
                    min-width: 26px;
                    height: 26px;
                    background: #1d4ed8;
                    color: #fff;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    font-size: 12px;
                    font-weight: 700;
                    flex-shrink: 0;
                    margin-top: 1px;
                }
                .opennit-steps li strong {
                    display: block;
                    color: #111827;
                    margin-bottom: 2px;
                }
                .opennit-steps code {
                    background: #f3f4f6;
                    padding: 1px 6px;
                    border-radius: 4px;
                    font-size: 12.5px;
                }
                .opennit-hint {
                    background: #eff6ff;
                    border: 1px solid #bfdbfe;
                    border-radius: 7px;
                    padding: 11px 15px;
                    font-size: 13px;
                    color: #1e40af;
                    margin-top: 16px;
                    display: flex;
                    align-items: flex-start;
                    gap: 8px;
                }
                .opennit-hint-warning {
                    background: #fffbeb;
                    border-color: #fde68a;
                    color: #92400e;
                }
                .opennit-hint-success {
                    background: #f0fdf4;
                    border-color: #bbf7d0;
                    color: #14532d;
                }
                .opennit-form-row {
                    margin-bottom: 20px;
                }
                .opennit-form-row:last-of-type {
                    margin-bottom: 0;
                }
                .opennit-form-row label {
                    display: block;
                    font-size: 13px;
                    font-weight: 600;
                    color: #374151;
                    margin-bottom: 6px;
                }
                .opennit-form-row input[type="url"],
                .opennit-form-row input[type="password"],
                .opennit-form-row input[type="text"] {
                    width: 100%;
                    max-width: 480px;
                    font-size: 13.5px;
                    padding: 7px 10px;
                    border: 1px solid #d1d5db;
                    border-radius: 6px;
                    box-shadow: none;
                }
                .opennit-form-row .field-hint {
                    font-size: 12.5px;
                    color: #6b7280;
                    margin-top: 5px;
                }
                .opennit-shortcodes {
                    border-collapse: collapse;
                    width: 100%;
                    font-size: 13.5px;
                }
                .opennit-shortcodes th {
                    text-align: left;
                    padding: 8px 12px;
                    background: #f9fafb;
                    border-bottom: 1px solid #e5e7eb;
                    font-size: 12px;
                    text-transform: uppercase;
                    letter-spacing: .04em;
                    color: #6b7280;
                    font-weight: 600;
                }
                .opennit-shortcodes td {
                    padding: 12px;
                    border-bottom: 1px solid #f3f4f6;
                    color: #374151;
                    vertical-align: top;
                }
                .opennit-shortcodes tr:last-child td {
                    border-bottom: none;
                }
                .opennit-shortcodes code {
                    background: #f3f4f6;
                    padding: 3px 8px;
                    border-radius: 5px;
                    font-size: 13px;
                    color: #1d4ed8;
                    white-space: nowrap;
                    cursor: pointer;
                }
                .opennit-shortcodes code:hover {
                    background: #dbeafe;
                }
                .opennit-test-row {
                    display: flex;
                    align-items: center;
                    gap: 14px;
                    margin-top: 4px;
                }
                #opennit-test-result {
                    font-size: 13.5px;
                    font-weight: 500;
                }
                .opennit-footer {
                    text-align: center;
                    padding: 12px 0 24px;
                    font-size: 12px;
                    color: #9ca3af;
                }
                .opennit-footer a {
                    color: #6b7280;
                    text-decoration: none;
                }
                .opennit-footer a:hover {
                    color: #1d4ed8;
                }
            </style>

            <!-- Header -->
            <div class="opennit-header">
                <div class="opennit-header-logo">
                    <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                        <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1 14V10l-2 1V9l3-1.5L14 9v2l-2-1v6h-1z"/>
                    </svg>
                </div>
                <div>
                    <h1>OpenNIT | Connector</h1>
                    <p>Verbindet WordPress mit der OpenNIT IT-Dokumentationsplattform</p>
                </div>
            </div>

            <?php if ($connected): ?>
            <div class="opennit-hint opennit-hint-success" style="margin-bottom:20px;">
                <span>✓</span>
                <span>Plugin ist verbunden und aktiv. Die Shortcodes können direkt auf Seiten und Beiträgen eingesetzt werden.</span>
            </div>
            <?php endif; ?>

            <!-- Schritt-für-Schritt-Einrichtung -->
            <div class="opennit-card">
                <h2><span class="card-icon">📋</span> Einrichtung in 4 Schritten</h2>
                <ol class="opennit-steps">
                    <li>
                        <div>
                            <strong>OpenNIT Backend öffnen</strong>
                            Melden Sie sich im OpenNIT Backend an und navigieren Sie zu
                            <strong>Admin → API</strong>.
                        </div>
                    </li>
                    <li>
                        <div>
                            <strong>API-URL kopieren</strong>
                            Auf der Seite <em>Admin → API</em> wird die fertige API-URL angezeigt.
                            Kopieren Sie diese und tragen Sie sie unten im Feld <em>API-Basis-URL</em> ein.
                        </div>
                    </li>
                    <li>
                        <div>
                            <strong>API-Token generieren</strong>
                            Klicken Sie im OpenNIT Backend auf <strong>„Neuen Token erstellen"</strong>.
                            Der Token wird <em>nur einmal</em> angezeigt — kopieren Sie ihn sofort und
                            tragen Sie ihn unten im Feld <em>API-Token</em> ein.
                        </div>
                    </li>
                    <li>
                        <div>
                            <strong>Speichern &amp; Verbindung testen</strong>
                            Klicken Sie auf <em>Einstellungen speichern</em> und anschließend auf
                            <em>Verbindung testen</em>, um die Verbindung zu prüfen.
                        </div>
                    </li>
                </ol>
                <div class="opennit-hint">
                    <span>💡</span>
                    <span>
                        <strong>Beschriftungen anpassen:</strong>
                        Menünamen, Seitenüberschriften und Texte der Shortcodes lassen sich direkt im
                        OpenNIT Backend unter <strong>Admin → API</strong> anpassen —
                        ohne Änderungen an WordPress oder diesem Plugin.
                    </span>
                </div>
            </div>

            <!-- Verbindungseinstellungen -->
            <div class="opennit-card">
                <h2><span class="card-icon">🔌</span> Verbindungseinstellungen</h2>
                <form method="post" action="options.php">
                    <?php settings_fields('opennit_settings'); ?>

                    <?php if (defined('OPENNIT_API_URL') || defined('OPENNIT_API_TOKEN')): ?>
                    <div class="opennit-hint opennit-hint-success" style="margin-bottom:18px;">
                        <span>🔒</span>
                        <span>
                            <strong>Konstanten aktiv:</strong>
                            API-URL<?= defined('OPENNIT_API_URL') ? ' ✓' : '' ?> und Token<?= defined('OPENNIT_API_TOKEN') ? ' ✓' : '' ?>
                            sind über <code>wp-config.php</code>-Konstanten gesetzt und können hier nicht überschrieben werden.
                        </span>
                    </div>
                    <?php endif; ?>

                    <div class="opennit-form-row">
                        <label for="opennit_api_url">API-Basis-URL</label>
                        <input type="url" id="opennit_api_url" name="opennit_api_url"
                               value="<?= esc_attr(defined('OPENNIT_API_URL') ? OPENNIT_API_URL : get_option('opennit_api_url', '')) ?>"
                               placeholder="https://opennit.beispiel.de/api/ex"
                               <?= defined('OPENNIT_API_URL') ? 'readonly' : '' ?>>
                        <p class="field-hint">
                            Die URL finden Sie im OpenNIT Backend unter <strong>Admin → WordPress Plugin</strong>.
                            Die URL muss mit <code>https://</code> beginnen — HTTP wird von der API abgewiesen.
                            Alternativ: Konstante <code>OPENNIT_API_URL</code> in <code>wp-config.php</code> setzen.
                        </p>
                    </div>

                    <div class="opennit-form-row">
                        <label for="opennit_api_token">API-Token</label>
                        <input type="password" id="opennit_api_token" name="opennit_api_token"
                               value="<?= esc_attr(defined('OPENNIT_API_TOKEN') ? OPENNIT_API_TOKEN : get_option('opennit_api_token', '')) ?>"
                               autocomplete="off"
                               <?= defined('OPENNIT_API_TOKEN') ? 'readonly' : '' ?>>
                        <p class="field-hint">
                            Token wird im OpenNIT Backend generiert und nur einmal angezeigt.
                            Bei Verlust muss ein neuer Token erstellt werden.
                            Alternativ: Konstante <code>OPENNIT_API_TOKEN</code> in <code>wp-config.php</code> setzen.
                        </p>
                    </div>

                    <div class="opennit-form-row">
                        <label for="opennit_api_signing_secret">Signing-Secret (HMAC)</label>
                        <input type="password" id="opennit_api_signing_secret" name="opennit_api_signing_secret"
                               value="<?= esc_attr(defined('OPENNIT_API_SIGNING_SECRET') ? OPENNIT_API_SIGNING_SECRET : get_option('opennit_api_signing_secret', '')) ?>"
                               autocomplete="off"
                               <?= defined('OPENNIT_API_SIGNING_SECRET') ? 'readonly' : '' ?>>
                        <p class="field-hint">
                            <strong>Pflicht.</strong> Wird beim Erstellen des Tokens im OpenNIT-Backend
                            <em>einmalig</em> zusammen mit dem Token angezeigt. Jede API-Anfrage wird damit
                            signiert (HMAC-SHA256) — Schutz vor Manipulation und Replay.
                            Alternativ: Konstante <code>OPENNIT_API_SIGNING_SECRET</code> in <code>wp-config.php</code>.
                        </p>
                    </div>

                    <div class="opennit-form-row">
                        <label for="opennit_ticket_page_url">URL der Ticket-Seite <span style="font-weight:400;color:#6b7280;">(optional)</span></label>
                        <input type="url" id="opennit_ticket_page_url" name="opennit_ticket_page_url"
                               value="<?= esc_attr(get_option('opennit_ticket_page_url', '')) ?>"
                               placeholder="/it-support">
                        <p class="field-hint">
                            URL der WordPress-Seite mit dem Shortcode <code>[opennit_ticket_form]</code>.
                            Wenn gesetzt, erscheint in der Geräteübersicht (<code>[opennit_assets]</code>) ein
                            „Ticket melden"-Button direkt am jeweiligen Gerät.
                        </p>
                    </div>

                    <hr style="margin:1.5rem 0;border:none;border-top:1px solid #e5e7eb;">
                    <h2 style="font-size:1.05rem;">KI-Assistent (Website-Bot)</h2>
                    <div class="opennit-hint" style="margin-bottom:0;">
                        <span>💡</span>
                        <span>
                            Die Einstellungen des Website-Bots (Bubble anzeigen, nur f&uuml;r angemeldete Nutzer,
                            Titel, Begr&uuml;&szlig;ungstext und zus&auml;tzlicher Kontext) werden jetzt zentral im
                            <strong>OpenNIT-Backend unter Admin &rarr; API</strong> gepflegt und vom Plugin
                            automatisch &uuml;bernommen. Der Bot muss dort zudem aktiviert sein (Scope
                            <code>assistant</code>, f&uuml;r Tickets zus&auml;tzlich <code>tickets</code>).
                            Einbinden per Bubble oder Shortcode <code>[opennit_assistant]</code>.
                        </span>
                    </div>

                    <?php submit_button('Einstellungen speichern', 'primary', 'submit', false); ?>

                    <div style="display:inline-flex; align-items:center; gap:14px; margin-left:12px;">
                        <button type="button" id="opennit-test-btn" class="button button-secondary">
                            Verbindung testen
                        </button>
                        <span id="opennit-test-result"></span>
                    </div>
                </form>
            </div>

            <!-- Shortcode-Übersicht -->
            <div class="opennit-card">
                <h2><span class="card-icon">{ }</span> Verfügbare Shortcodes</h2>
                <p style="font-size:13px; color:#6b7280; margin:0 0 16px;">
                    Shortcodes können auf beliebigen WordPress-Seiten oder Beiträgen eingesetzt werden.
                    Klicken Sie auf einen Shortcode, um ihn in die Zwischenablage zu kopieren.
                </p>
                <table class="opennit-shortcodes">
                    <thead>
                        <tr>
                            <th style="width:230px;">Shortcode</th>
                            <th>Was wird angezeigt</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_loan_devices]')" title="Klicken zum Kopieren">[opennit_loan_devices]</code></td>
                            <td>
                                Leihgeräte-Katalog, aktive Leihen des angemeldeten Nutzers und
                                Anfrageformular — alles in einer vollbreiten Ansicht.
                            </td>
                        </tr>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_assets]')" title="Klicken zum Kopieren">[opennit_assets]</code></td>
                            <td>
                                Persönliche IT-Geräteübersicht des angemeldeten Nutzers
                                (Zuordnung über die WordPress-E-Mail-Adresse), inkl. Standort und Garantie.
                            </td>
                        </tr>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_ticket_form]')" title="Klicken zum Kopieren">[opennit_ticket_form]</code></td>
                            <td>
                                IT-Support-Ticket-Formular — Einsender kann ein neues Ticket mit
                                Kategorie, Standort, optionalem Gerätebezug und Dateianhang erstellen.
                            </td>
                        </tr>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_assistant]')" title="Klicken zum Kopieren">[opennit_assistant]</code></td>
                            <td>
                                KI-Assistent als eingebettetes Chat-Fenster. Beantwortet einfache PC-Fragen
                                und Fragen zur freigegebenen Dokumentation. Alternativ als schwebende Bubble
                                (siehe Einstellung oben). Benötigt Token-Scope <code>assistant</code>.
                            </td>
                        </tr>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_my_tickets]')" title="Klicken zum Kopieren">[opennit_my_tickets]</code></td>
                            <td>
                                Übersicht der eigenen Tickets mit Detail-Ansicht, Antwortenverlauf,
                                Ansprechpartner, Beobachter, verknüpftem Asset und Antwort-Funktion.
                            </td>
                        </tr>
                        <tr>
                            <td><code onclick="navigator.clipboard.writeText('[opennit_wiki]')" title="Klicken zum Kopieren">[opennit_wiki]</code></td>
                            <td>
                                Wissensdatenbank-Shortcode: zeigt alle freigegebenen Themen als
                                anklickbare Kacheln, Unterthemen und Seiteninhalte. Inhalte werden
                                24 Stunden gecacht (inkl. Bilder).<br><br>
                                <strong>Optionales Attribut <code>topic="ID"</code>:</strong>
                                <code>[opennit_wiki topic="5"]</code> legt ein bestimmtes Thema als festen
                                Einstiegspunkt fest. Die globale Themenübersicht wird übersprungen; die Seite
                                zeigt direkt die Unterthemen und Seiten dieses Themas. Der Zurück-Button wird
                                ausgeblendet, solange man sich auf der Wurzelebene befindet. Navigation in
                                Unterthemen funktioniert wie gewohnt – der Zurück-Link führt dann eine Ebene
                                höher, maximal aber zum konfigurierten Wurzelthema.
                            </td>
                        </tr>
                    </tbody>
                </table>
                <div class="opennit-hint opennit-hint-warning" style="margin-top:16px;">
                    <span>⚠</span>
                    <span>
                        Die Shortcodes setzen voraus, dass der Nutzer in WordPress eingeloggt ist.
                        Die E-Mail-Adresse des WordPress-Accounts muss mit dem Eintrag in OpenNIT übereinstimmen.
                    </span>
                </div>
            </div>

            <!-- Cache -->
            <div class="opennit-card">
                <h2><span class="card-icon">📚</span> Cache leeren</h2>
                <p style="font-size:13px; color:#6b7280; margin:0 0 16px;">
                    Der <code>[opennit_wiki]</code>-Shortcode speichert Themen, Seiten und Bilder
                    für <strong>24 Stunden</strong>; Plugin-Einstellungen (Menüname, Texte und die
                    <strong>Assistent-/Bubble-Einstellungen</strong>) werden für <strong>5 Minuten</strong>
                    gecacht. Nach Änderungen im OpenNIT-Backend (z. B. Bubble aktivieren) können Sie den
                    Cache hier sofort leeren, damit die Änderungen ohne Wartezeit wirksam werden.
                </p>
                <div style="display:flex; align-items:center; gap:14px;">
                    <button type="button" id="opennit-flush-wiki-btn" class="button button-secondary">
                        Cache jetzt leeren
                    </button>
                    <span id="opennit-flush-wiki-result" style="font-size:13.5px; font-weight:500;"></span>
                </div>
            </div>

            <!-- API-Diagnose -->
            <div class="opennit-card">
                <h2><span class="card-icon">🔍</span> API-Diagnose</h2>
                <p style="font-size:13px; color:#6b7280; margin:0 0 16px;">
                    Prüft jeden API-Endpunkt einzeln und zeigt Status, HTTP-Code und Antwort.
                    Hilfreich wenn Dropdowns nicht laden oder Daten fehlen.
                </p>
                <div style="display:flex; align-items:center; gap:14px; margin-bottom:12px;">
                    <button type="button" id="opennit-diagnose-btn" class="button button-secondary">
                        Diagnose starten
                    </button>
                    <span id="opennit-diagnose-status" style="font-size:13px; color:#6b7280;"></span>
                </div>
                <div id="opennit-diagnose-result" style="display:none; font-size:13px; line-height:1.7;"></div>
            </div>

            <div class="opennit-footer">
                <a href="https://opennit.de" target="_blank">opennit.de</a>
                &nbsp;·&nbsp; OpenNIT | Connector v<?= esc_html(OPENNIT_VERSION) ?>
            </div>

        </div>

        <script>
        document.getElementById('opennit-test-btn').addEventListener('click', function() {
            var btn    = this;
            var result = document.getElementById('opennit-test-result');
            btn.disabled = true;
            result.textContent = 'Teste Verbindung…';
            result.style.color = '#6b7280';

            var formData = new FormData();
            formData.append('action', 'opennit_test_connection');
            formData.append('nonce', '<?= wp_create_nonce('opennit_test') ?>');

            fetch(ajaxurl, { method: 'POST', body: formData })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.success) {
                        result.innerHTML = '✓ Verbindung erfolgreich';
                        result.style.color = '#16a34a';
                    } else {
                        result.innerHTML = '✗ Fehler: ' + (data.data || 'Unbekannter Fehler');
                        result.style.color = '#dc2626';
                    }
                })
                .catch(function() {
                    result.innerHTML = '✗ Netzwerkfehler';
                    result.style.color = '#dc2626';
                })
                .finally(function() {
                    btn.disabled = false;
                });
        });

        document.getElementById('opennit-flush-wiki-btn').addEventListener('click', function() {
            var btn    = this;
            var result = document.getElementById('opennit-flush-wiki-result');
            btn.disabled = true;
            result.textContent = 'Cache wird geleert…';
            result.style.color = '#6b7280';

            var formData = new FormData();
            formData.append('action', 'opennit_flush_wiki_cache');
            formData.append('nonce', '<?= wp_create_nonce('opennit_flush_wiki') ?>');

            fetch(ajaxurl, { method: 'POST', body: formData })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.success) {
                        result.innerHTML = '✓ Cache geleert';
                        result.style.color = '#16a34a';
                    } else {
                        result.innerHTML = '✗ Fehler: ' + (data.data || 'Unbekannter Fehler');
                        result.style.color = '#dc2626';
                    }
                })
                .catch(function() {
                    result.innerHTML = '✗ Netzwerkfehler';
                    result.style.color = '#dc2626';
                })
                .finally(function() {
                    btn.disabled = false;
                });
        });

        document.getElementById('opennit-diagnose-btn').addEventListener('click', function() {
            var btn    = this;
            var status = document.getElementById('opennit-diagnose-status');
            var result = document.getElementById('opennit-diagnose-result');
            btn.disabled = true;
            status.textContent = 'Diagnose läuft…';
            status.style.color = '#6b7280';
            result.style.display = 'none';
            result.innerHTML = '';

            var formData = new FormData();
            formData.append('action', 'opennit_diagnose');
            formData.append('nonce', '<?= wp_create_nonce('opennit_diagnose') ?>');

            fetch(ajaxurl, { method: 'POST', body: formData })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.success) {
                        result.innerHTML = data.data.html;
                        result.style.display = 'block';
                        status.textContent = '';
                    } else {
                        status.textContent = '✗ ' + (data.data || 'Fehler');
                        status.style.color = '#dc2626';
                    }
                })
                .catch(function() {
                    status.textContent = '✗ Netzwerkfehler';
                    status.style.color = '#dc2626';
                })
                .finally(function() {
                    btn.disabled = false;
                });
        });
        </script>
        <?php
    }

    public function ajax_test_connection(): void {
        check_ajax_referer('opennit_test', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Keine Berechtigung.');
            return;
        }

        $client = new OpenNIT_ApiClient();
        $result = $client->test_connection();

        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        wp_send_json_success('OK');
    }

    public function ajax_flush_wiki_cache(): void {
        check_ajax_referer('opennit_flush_wiki', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Keine Berechtigung.');
            return;
        }

        OpenNIT_Wiki::flush_all();
        // Auch den Konfigurations-Cache leeren – darüber kommen u. a. die
        // Assistent-Einstellungen (Bubble, Titel, Begrüßung) aus dem OpenNIT-Backend.
        delete_transient('opennit_config_cache');
        delete_transient('opennit_menu_name_synced');
        wp_send_json_success('Cache geleert.');
    }

    public function ajax_diagnose(): void {
        check_ajax_referer('opennit_diagnose', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Keine Berechtigung.');
            return;
        }

        $api_url = rtrim((string)(defined('OPENNIT_API_URL') ? OPENNIT_API_URL : get_option('opennit_api_url', '')), '/');
        $api_url = preg_replace('#^http://#', 'https://', $api_url);
        $token   = (string)(defined('OPENNIT_API_TOKEN') ? OPENNIT_API_TOKEN : get_option('opennit_api_token', ''));
        $secret  = (string)(defined('OPENNIT_API_SIGNING_SECRET') ? OPENNIT_API_SIGNING_SECRET : get_option('opennit_api_signing_secret', ''));

        if (!$api_url || !$token) {
            wp_send_json_error('API-URL oder Token nicht konfiguriert.');
            return;
        }
        if (!$secret) {
            wp_send_json_error('Signing-Secret nicht konfiguriert. Bitte eintragen (wird beim Token-Erstellen im OpenNIT-Backend angezeigt).');
            return;
        }

        // Alle Aufrufe laufen über den ApiClient, damit sie korrekt signiert werden.
        $client    = new OpenNIT_ApiClient();
        $endpoints = [
            ['label' => 'Konfiguration',     'path' => '/config',           'call' => fn() => $client->get_config(),           'key' => 'texts'],
            ['label' => 'Ticket-Standorte',  'path' => '/ticket-locations', 'call' => fn() => $client->get_ticket_locations(), 'key' => 'locations'],
            ['label' => 'Ticket-Kategorien', 'path' => '/ticket-categories','call' => fn() => $client->get_ticket_categories(),'key' => 'categories'],
            ['label' => 'Leihgeräte',        'path' => '/loan-devices',     'call' => fn() => $client->get_loan_devices(),     'key' => 'devices'],
            ['label' => 'Gerätetypen',       'path' => '/device-types',     'call' => fn() => $client->get_device_types(),     'key' => 'types'],
        ];

        $rows = [];
        foreach ($endpoints as $ep) {
            $result = ($ep['call'])();

            if (is_wp_error($result)) {
                $code = '—';
                if (preg_match('/api_error_(\d+)/', $result->get_error_code(), $m)) {
                    $code = (int) $m[1];
                }
                $rows[] = [
                    'label'  => $ep['label'],
                    'path'   => $ep['path'],
                    'code'   => $code,
                    'ok'     => false,
                    'detail' => $result->get_error_message(),
                ];
                continue;
            }

            $key = $ep['key'];
            if (isset($result[$key]) && is_array($result[$key])) {
                $count  = count($result[$key]);
                $detail = $key . ': ' . $count . ' Einträge';
                $ok     = $count > 0;
                if ($count === 0) {
                    $detail .= ' ⚠ Tabelle leer oder Scope fehlt?';
                }
            } else {
                $detail = 'JSON OK (' . implode(', ', array_keys($result)) . ')';
                $ok     = true;
            }

            $rows[] = [
                'label'  => $ep['label'],
                'path'   => $ep['path'],
                'code'   => $ok ? 200 : '—',
                'ok'     => $ok,
                'detail' => $detail,
            ];
        }

        $html  = '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
        $html .= '<tr style="background:#f9fafb;">';
        $html .= '<th style="text-align:left;padding:6px 10px;border-bottom:1px solid #e5e7eb;white-space:nowrap;">Endpunkt</th>';
        $html .= '<th style="text-align:left;padding:6px 10px;border-bottom:1px solid #e5e7eb;font-family:monospace;font-size:12px;">Pfad</th>';
        $html .= '<th style="text-align:center;padding:6px 10px;border-bottom:1px solid #e5e7eb;white-space:nowrap;">HTTP</th>';
        $html .= '<th style="text-align:left;padding:6px 10px;border-bottom:1px solid #e5e7eb;">Ergebnis</th>';
        $html .= '</tr>';

        foreach ($rows as $r) {
            $color = $r['ok'] ? '#16a34a' : '#dc2626';
            $icon  = $r['ok'] ? '✓' : '✗';
            $html .= '<tr>';
            $html .= '<td style="padding:6px 10px;border-bottom:1px solid #f3f4f6;white-space:nowrap;">' . esc_html($r['label']) . '</td>';
            $html .= '<td style="padding:6px 10px;border-bottom:1px solid #f3f4f6;font-family:monospace;font-size:12px;">' . esc_html($r['path']) . '</td>';
            $html .= '<td style="padding:6px 10px;border-bottom:1px solid #f3f4f6;text-align:center;color:' . $color . ';font-weight:700;">' . esc_html((string)$r['code']) . '</td>';
            $html .= '<td style="padding:6px 10px;border-bottom:1px solid #f3f4f6;color:' . $color . ';">' . $icon . ' ' . esc_html($r['detail']) . '</td>';
            $html .= '</tr>';
        }

        $html .= '</table>';
        $html .= '<p style="margin:10px 0 0;font-size:12px;color:#9ca3af;">API-URL: <code>' . esc_html($api_url) . '</code></p>';

        wp_send_json_success(['html' => $html]);
    }

    public function show_notices(): void {
        $screen = get_current_screen();
        if (!$screen || $screen->id !== 'toplevel_page_opennit-connector') {
            return;
        }

        $url    = get_option('opennit_api_url', '');
        $token  = get_option('opennit_api_token', '');
        $secret = defined('OPENNIT_API_SIGNING_SECRET') ? OPENNIT_API_SIGNING_SECRET : get_option('opennit_api_signing_secret', '');

        if (empty($url) || empty($token) || empty($secret)) {
            $plugin_name = esc_html(get_option('opennit_menu_name', 'OpenNIT | Connector'));
            echo '<div class="notice notice-warning"><p><strong>' . $plugin_name . ':</strong> Bitte API-URL, Token und Signing-Secret konfigurieren.</p></div>';
        }
    }

    public function enqueue_admin_scripts(string $hook): void {
        // Nur auf unserer Settings-Seite
        if ($hook !== 'settings_page_opennit-connector') {
            return;
        }
    }
}