OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

71 KB
<?php $this->layout('layouts/admin', ['page_title' => $page_title, 'current_admin_page' => 'api', 'extra_css' => ['/admin/api/swagger-ui.css']]); ?>
<style>
/* ── Tab Content ─────────────────────────────────────── */
.api-tab-content {
    display: none;
}
.api-tab-content.active {
    display: block;
}

/* ── Layout ──────────────────────────────────────────── */
.wp-section   { margin-bottom: 1.75rem; }
.wp-grid-2    { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 860px) { .wp-grid-2 { grid-template-columns: 1fr; } }

/* ── Card ────────────────────────────────────────────── */
.wp-card             { background: var(--color-bg); border: 1px solid var(--gray-200); border-radius: 0.5rem; overflow: hidden; }
.wp-card-header      { padding: 0.875rem 1.25rem; border-bottom: 1px solid var(--gray-200);
                        display: flex; align-items: center; justify-content: space-between; gap: 1rem;
                        background: var(--gray-50); }
.wp-card-header h2   { margin: 0; font-size: 0.95rem; font-weight: 700; color: var(--gray-900); display: flex; align-items: center; gap: 0.5rem; }
.wp-card-header h2 .wp-card-icon { opacity: .7; }
.wp-card-body        { padding: 1.25rem; }
.wp-card-footer      { padding: 0.875rem 1.25rem; border-top: 1px solid var(--gray-100); background: var(--gray-50);
                        display: flex; align-items: center; gap: 0.75rem; }

/* Buttons kommen aus app.css (.btn + Varianten) */

/* ── Table ───────────────────────────────────────────── */
.wp-table              { width: 100%; border-collapse: collapse; }
.wp-table th           { text-align: left; padding: 0.6rem 1rem; background: var(--gray-50);
                          border-bottom: 1px solid var(--gray-200);
                          font-size: 0.73rem; font-weight: 700; text-transform: uppercase;
                          letter-spacing: 0.04em; color: var(--gray-500); }
.wp-table td           { padding: 0.75rem 1rem; border-bottom: 1px solid var(--gray-100);
                          font-size: 0.875rem; color: var(--gray-700); vertical-align: middle; }
.wp-table tr:last-child td { border-bottom: none; }
.wp-table tr:hover td  { background: var(--gray-50); }

/* ── Badges ──────────────────────────────────────────── */
.wp-badge         { display: inline-block; padding: 0.2rem 0.55rem; border-radius: 9999px; font-size: 0.73rem; font-weight: 600; }
.wp-badge-active  { background: var(--green-100); color: var(--green-700); }
.wp-badge-revoked { background: var(--red-100); color: var(--red-700); }

/* ── Misc ────────────────────────────────────────────── */
.wp-code       { font-family: monospace; font-size: 0.85rem; background: var(--gray-100);
                  padding: 0.15rem 0.45rem; border-radius: 0.2rem; color: var(--gray-800); }
.wp-empty      { text-align: center; padding: 2rem 1rem; color: var(--gray-400); font-size: 0.875rem; }
.wp-divider    { border: none; border-top: 1px solid var(--gray-100); margin: 1.25rem 0; }

/* ── Token-Alert ──────────────────────────────────────── */
.wp-token-alert        { background: var(--yellow-50); border: 1px solid var(--yellow-300); border-radius: 0.5rem;
                          padding: 1rem 1.25rem; margin-bottom: 1.5rem; }
.wp-token-alert strong { display: block; margin-bottom: 0.5rem; color: var(--yellow-900); }
.wp-token-value        { font-family: monospace; font-size: 0.85rem; background: var(--color-bg);
                          border: 1px solid var(--yellow-300); border-radius: 0.25rem;
                          padding: 0.5rem 0.75rem; word-break: break-all; user-select: all;
                          color: var(--gray-800); margin: 0.5rem 0; cursor: text; }

/* ── Setup info box ──────────────────────────────────── */
.wp-setup-info     { background: var(--sky-50); border: 1px solid var(--sky-200); border-radius: 0.5rem;
                      padding: 1rem 1.25rem; }
.wp-setup-info h3  { margin: 0 0 0.75rem; font-size: 0.9rem; color: var(--sky-900); font-weight: 700; }
.wp-setup-info dl  { margin: 0; display: grid; grid-template-columns: auto 1fr;
                      gap: 0.4rem 1rem; align-items: center; }
.wp-setup-info dt  { font-size: 0.8rem; color: var(--gray-500); font-weight: 600; white-space: nowrap; }
.wp-setup-info dd  { margin: 0; }
.wp-setup-note     { margin: 0.75rem 0 0; font-size: 0.82rem; color: var(--sky-700); }

/* ── Token form ──────────────────────────────────────── */
.wp-form-row   { display: flex; gap: 0.5rem; align-items: flex-end; flex-wrap: wrap; }
/* Formulargruppen: zentrale .form-group/.form-control; Lesebreite begrenzt */
.wp-text-fields .form-control { max-width: 640px; }
.wp-section-label   { font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
                        letter-spacing: 0.06em; color: var(--gray-400); margin: 0 0 0.85rem; }

/* ── API Doku ────────────────────────────────────────── */
.api-intro { background: var(--emerald-50); border: 1px solid var(--emerald-200); border-radius: 0.5rem;
             padding: 1.25rem; margin-bottom: 1.5rem; }
.api-intro h3 { margin: 0 0 0.5rem; color: var(--emerald-800); font-size: 0.95rem; }
.api-intro p { margin: 0 0 0.5rem; color: var(--emerald-700); font-size: 0.875rem; line-height: 1.5; }
.api-intro p:last-child { margin: 0; }

.api-docs-section { margin-bottom: 1.75rem; }
.api-docs-section h3 { margin: 0 0 0.75rem; font-size: 0.9rem; font-weight: 700; color: var(--gray-900);
                       border-bottom: 2px solid var(--gray-100); padding-bottom: 0.5rem; }

.api-endpoint { display: grid; grid-template-columns: 80px 1fr; gap: 1rem; margin-bottom: 1rem;
                padding: 1rem; background: var(--gray-50); border-radius: 0.3rem; border-left: 3px solid var(--color-primary); }
.api-method { font-weight: 700; font-size: 0.75rem; text-transform: uppercase;
              display: inline-block; padding: 0.25rem 0.5rem; background: var(--blue-100); color: var(--blue-800);
              border-radius: 0.2rem; align-self: start; }
.api-details h4 { margin: 0 0 0.35rem; font-size: 0.875rem; color: var(--gray-900); }
.api-details p { margin: 0; font-size: 0.8rem; color: var(--gray-500); }
.api-path { font-family: monospace; font-size: 0.8rem; background: var(--color-bg); padding: 0.35rem 0.5rem;
            border-radius: 0.2rem; color: var(--gray-800); display: inline-block; margin: 0.25rem 0; }

.api-integration { background: var(--gray-100); border: 1px solid var(--gray-300); border-radius: 0.5rem;
                   padding: 1rem 1.25rem; margin-top: 1.25rem; }
.api-integration h4 { margin: 0 0 0.5rem; font-size: 0.9rem; color: var(--gray-900); }
.api-integration p { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--gray-600); line-height: 1.4; }
.api-integration p:last-child { margin: 0; }

/* ── Request Body ────────────────────────────────────── */
.api-body-label {
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--gray-400); margin-top: 0.6rem; margin-bottom: 0.2rem;
}
.api-body {
    background: var(--code-dark-bg); color: var(--code-dark-text); border-radius: 0.3rem;
    padding: 0.65rem 0.9rem; font-family: monospace; font-size: 0.78rem;
    line-height: 1.6; overflow-x: auto; white-space: pre; margin: 0;
}
.api-body .key   { color: var(--code-dark-cyan); }
.api-body .val   { color: var(--code-dark-green); }
.api-body .opt   { color: var(--code-dark-muted); }
.api-fields { margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.3rem; }
.api-field-req  { font-size: 0.72rem; background: var(--amber-100); color: var(--amber-800);
                  padding: 0.15rem 0.45rem; border-radius: 0.2rem; }
.api-field-opt  { font-size: 0.72rem; background: var(--gray-100); color: var(--gray-500);
                  padding: 0.15rem 0.45rem; border-radius: 0.2rem; }

/* ── Scope Badge ─────────────────────────────────────── */
.scope-badge    { font-size:0.72rem;font-weight:700;background:var(--indigo-100);color:var(--indigo-700);
                  padding:0.15rem 0.5rem;border-radius:0.2rem;vertical-align:middle;
                  margin-left:0.4rem;letter-spacing:0.01em; }

/* ── Response / Params ───────────────────────────────── */
.api-resp-label { font-size:0.72rem;font-weight:700;text-transform:uppercase;
                  letter-spacing:0.05em;color:var(--gray-500);margin-top:0.6rem;margin-bottom:0.2rem; }
.api-params     { margin:0.4rem 0 0.1rem;display:flex;flex-direction:column;gap:0.15rem; }
.api-param      { font-size:0.8rem;color:var(--gray-600); }
.api-param code { background:var(--gray-100);padding:0.1rem 0.3rem;border-radius:0.15rem;font-size:0.78rem; }
.api-param-req  { font-size:0.7rem;font-weight:700;color:var(--amber-700);background:var(--amber-100);
                  padding:0.05rem 0.3rem;border-radius:0.15rem;margin:0 0.25rem; }
.api-param-opt  { font-size:0.7rem;font-weight:700;color:var(--gray-500);background:var(--gray-100);
                  padding:0.05rem 0.3rem;border-radius:0.15rem;margin:0 0.25rem; }

/* ── Responsive ──────────────────────────────────────── */

/* Tablet / narrow desktop (≤ 900px) */
@media (max-width: 900px) {
    .wp-card-header      { flex-wrap: wrap; }
    }

/* Mobile (≤ 640px) */
@media (max-width: 640px) {
    /* Card: tighter padding */
    .wp-card-body   { padding: 1rem 0.875rem; }
    .wp-card-header { padding: 0.75rem 0.875rem; }
    .wp-card-footer { padding: 0.75rem 0.875rem; }

    /* Card header: stack heading + button vertically */
    .wp-card-header h2  { font-size: 0.875rem; }

    /* Setup info: stack dt / dd */
    .wp-setup-info { padding: 0.875rem 1rem; }
    .wp-setup-info dl { grid-template-columns: 1fr; row-gap: 0.2rem; }
    .wp-setup-info dt { margin-top: 0.65rem; }
    .wp-setup-info dt:first-child { margin-top: 0; }

    /* wp-code: allow long strings to wrap */
    .wp-code { white-space: normal; word-break: break-all; }

    /* Token name input: full width */
    #token_name { max-width: 100% !important; }

    /* Selects (booking rules): full width */
    .wp-text-fields .form-control { max-width: 100%; }

    /* API endpoint: stack method badge above details */
    .api-endpoint { grid-template-columns: 1fr; gap: 0.4rem; }

    /* API docs code blocks: horizontal scroll */
    .api-body { font-size: 0.73rem; }

    /* API intro: stack description + download button */
    .api-intro { display: block !important; }
    .api-intro > a.btn { margin-top: 0.75rem; }

    /* Section label: no uppercase overflow */
    .wp-section-label { letter-spacing: 0.03em; }

    /* Token alert value: ensure word-break on very long tokens */
    .wp-token-value { font-size: 0.78rem; }

    /* Save / submit row: stack */
    div[style*="display:flex"][style*="gap:1rem"] { flex-wrap: wrap; }
}
</style>

<!-- ═══ Tab System ═══════════════════════════════════════════════ -->
<nav class="settings-tabs">
    <a href="#" class="settings-tab active" data-tab="token-management">
        <i class="fas fa-key settings-tab-icon"></i> <?= $this->te('Token-Verwaltung') ?>
    </a>
    <a href="#" class="settings-tab" data-tab="wordpress-plugin">
        <i class="fas fa-globe settings-tab-icon"></i> <?= $this->te('WordPress Plugin') ?>
    </a>
    <a href="#" class="settings-tab" data-tab="access-control">
        <i class="fas fa-shield-alt settings-tab-icon"></i> <?= $this->te('Zugriffskontrolle') ?>
    </a>
    <a href="#" class="settings-tab" data-tab="api-docs">
        <i class="fas fa-code settings-tab-icon"></i> <?= $this->te('API-Dokumentation') ?>
    </a>
</nav>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 1: API-Dokumentation (Swagger UI)                           -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="api-docs" class="api-tab-content">
    <style>
        /* ── Kollisionen mit app.css beheben ──────────────────────────────────
           app.css definiert .wrapper { min-height:100vh; background:… } als
           globale Klasse. Swagger UI nutzt .wrapper intern an mehreren Stellen
           (Hauptansicht UND .auth-container im Authorize-Modal) – ohne Reset
           würde jedes dieser Wrapper-Divs auf volle Bildschirmhöhe aufgebläht.
           Heading/a/code/pre brauchen keinen Override: Swagger's eigene Regeln
           (.swagger-ui h3 etc., Spez. 0,1,1) schlagen app.css (0,0,1) bereits.
           Nur `p` ist nicht explizit in Swagger's Reset-Scope – daher separat. */
        #swagger-ui-container .swagger-ui .wrapper {
            min-height: 0;
            background: transparent;
            position: static;
            padding: 0;
            max-width: none;
        }
        /* p: Swagger hat kein globales .swagger-ui p im Reset –
           app.css { margin: 0 0 1rem } würde sonst durchschlagen          */
        #swagger-ui-container .swagger-ui p { margin: 0 0 0.5em; }

        /* ── Topbar ausblenden ────────────────────────────────────────────── */
        #swagger-ui-container .swagger-ui .topbar { display: none; }

        /* ── API-Info-Bereich ─────────────────────────────────────────────── */
        #swagger-ui-container .swagger-ui { font-family: inherit; }
        #swagger-ui-container .swagger-ui .info { margin: 0.75rem 0 1rem; }
        #swagger-ui-container .swagger-ui .info .title { font-size: 1.25rem !important; color: var(--gray-900); }

        /* ── Server-Auswahl & Authorize-Button ───────────────────────────────
           Swagger-Standard: padding:30px 0, box-shadow, margin:0 0 20px.
           Das interne label hat margin:-20px (auf 30px abgestimmt) – bei
           kompaktem Padding muss es resettet werden, sonst zieht es die
           Beschriftung aus der Box heraus.                                 */
        #swagger-ui-container .swagger-ui .scheme-container {
            background: var(--gray-50);
            border: 1px solid var(--gray-200);
            border-radius: 0.5rem;
            padding: 0.625rem 1rem;
            margin: 0 0 1rem;
            box-shadow: none;
        }
        #swagger-ui-container .swagger-ui .scheme-container .schemes > .schemes-server-container > label {
            margin: 0 1rem 0 0;
        }

        /* ── Container-Mindesthöhe + Lade-Animation ───────────────────────── */
        #swagger-ui-container { min-height: 400px; }
        .swagger-ui-loading { display: flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 3rem 1rem; color: var(--gray-500); font-size: 0.875rem; }
        .swagger-ui-loading-spinner { width: 1.25rem; height: 1.25rem; border: 2px solid var(--gray-200); border-top-color: var(--color-primary); border-radius: 50%; animation: swui-spin 0.7s linear infinite; }
        @keyframes swui-spin { to { transform: rotate(360deg); } }
    </style>

    <div class="api-intro" style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;">
        <div>
            <h3 style="margin:0 0 0.25rem;"><?= $this->te('Swagger UI – Interaktive API-Dokumentation') ?></h3>
            <p style="margin:0;font-size:0.875rem;color:var(--emerald-700);"><?= $this->t('Alle Endpunkte können hier direkt ausprobiert werden. Token unter <strong>Token-Verwaltung</strong> erstellen, dann oben auf <strong>Authorize</strong> klicken.') ?></p>
        </div>
        <a href="/admin/api/docs-download" class="btn btn-default" style="flex-shrink:0;white-space:nowrap;align-self:flex-start;">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
            <?= $this->te('Doku als .md herunterladen') ?>
        </a>
    </div>

    <div id="swagger-ui-container">
        <div class="swagger-ui-loading">
            <div class="swagger-ui-loading-spinner"></div>
            <?= $this->te('Swagger UI wird geladen…') ?>
        </div>
    </div>
</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 2: Token-Verwaltung                                         -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="token-management" class="api-tab-content active">

<?php if (!empty($new_token)): ?>
<div class="wp-token-alert">
    <?= $this->t('<strong>&#9888; Neuer API-Token erstellt – bitte jetzt kopieren!</strong> Dieser Token wird nur einmal angezeigt und kann danach nicht mehr abgerufen werden.') ?>
    <div class="wp-token-value" title="<?= $this->te('Klicken zum Auswählen') ?>"><?= $this->e($new_token) ?></div>
    <small style="color:var(--amber-800);"><?= $this->te('Tragen Sie diesen Token in Ihre API-Client-Einstellungen ein (z.B. WordPress Plugin, Dify, n8n, etc.).') ?></small>
    <?php if (!empty($new_signing_secret)): ?>
    <div style="margin-top:0.9rem;">
        <strong><?= $this->te('&#128273; Signing-Secret (HMAC) – ebenfalls einmalig:') ?></strong>
        <div class="wp-token-value" title="<?= $this->te('Klicken zum Auswählen') ?>"><?= $this->e($new_signing_secret) ?></div>
        <small style="color:var(--amber-800);">
            <?= $this->t('Pflicht für alle Endpunkte außer den Agent-Endpunkten (IPAM-Scan, DNS-Import): Jede API-Anfrage wird damit signiert (Schutz vor Manipulation/Replay). Token <em>und</em> Signing-Secret beim aufrufenden System hinterlegen – beim WordPress-Plugin in dessen Einstellungen. Ohne Signatur antwortet die API mit 401.') ?>
        </small>
    </div>
    <?php endif; ?>
</div>
<?php endif; ?>

<div class="wp-section">
    <div class="wp-card">
        <div class="wp-card-header">
            <h2>
                <svg class="wp-card-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
                <?= $this->te('API-Tokens') ?>
            </h2>
        </div>
        <div class="wp-card-body">

            <!-- Neuen Token erstellen -->
            <form method="POST" action="/admin/api/tokens" style="margin-bottom:1.5rem;">
                <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token ?? '') ?>">
                <div class="form-group" style="margin-bottom:0.75rem;">
                    <label for="token_name"><?= $this->te('Neuen API-Token erstellen') ?></label>
                    <input type="text" class="form-control" id="token_name" name="token_name"
                           placeholder="<?= $this->te('z.B. Dify Integration, n8n Workflow') ?>" required maxlength="200"
                           style="max-width:360px;">
                </div>
                <div style="margin-bottom:0.75rem;">
                    <div style="font-size:0.8rem;font-weight:600;color:var(--gray-700);margin-bottom:0.4rem;"><?= $this->te('Berechtigungen') ?></div>
                    <div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
                        <?php
                        // Die Bereiche kommen aus ApiScopes – eine zweite Liste hier
                        // ginge mit der ersten auseinander.
                        $gruppen = [];
                        foreach ($api_scopes as $key => $info) {
                            $gruppen[$info['gruppe']][$key] = $info;
                        }
                        ?>
                        <div style="display:flex;flex-direction:column;gap:0.9rem;width:100%;">
                        <?php foreach ($gruppen as $gruppe => $bereiche): ?>
                            <div>
                                <div style="font-size:0.75rem;font-weight:600;color:var(--gray-500);margin-bottom:0.35rem;">
                                    <?= $this->e($gruppe) ?>
                                </div>
                                <div style="display:flex;flex-wrap:wrap;gap:0.5rem;">
                                <?php foreach ($bereiche as $key => $info): ?>
                                    <label title="<?= $this->e($info['desc']) ?>"
                                           style="display:inline-flex;align-items:center;gap:0.3rem;font-size:0.82rem;
                                                  background:var(--gray-100);padding:0.3rem 0.6rem;border-radius:0.25rem;cursor:pointer;
                                                  border:1px solid var(--gray-200);">
                                        <input type="checkbox" name="scopes[]" value="<?= $this->e((string)$key) ?>"
                                               style="width:0.875rem;height:0.875rem;">
                                        <?= $this->te($info['label']) ?><?php if (!empty($info['explizit'])): ?>
                                        <span title="<?= $this->te('Wird von einem alten Vollzugriff-Token nicht abgedeckt') ?>">&#9888;</span>
                                        <?php endif; ?>
                                    </label>
                                <?php endforeach; ?>
                                </div>
                            </div>
                        <?php endforeach; ?>
                        </div>
                    </div>
                    <p style="font-size:0.75rem;color:var(--gray-500);margin:0.35rem 0 0;">
                        <?= $this->te('Mindestens ein Bereich muss gewählt sein. Ein Bereich gilt für die ganze Sammlung: Anders als eine angemeldete Person kennt ein Zugangsschlüssel keine Standort-Rechte.') ?>
                    </p>
                </div>
                <button type="submit" class="btn btn-primary">
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
                    <?= $this->te('Token erstellen') ?>
                </button>
            </form>

            <!-- Token-Tabelle -->
            <?php if (empty($tokens)): ?>
                <div class="wp-empty"><?= $this->te('&#128273; Noch keine API-Tokens erstellt.') ?></div>
            <?php else: ?>
            <div style="overflow-x:auto;">
                <table class="wp-table">
                    <thead>
                        <tr>
                            <th scope="col"><?= $this->te('Name') ?></th>
                            <th scope="col"><?= $this->te('Berechtigungen') ?></th>
                            <th scope="col"><?= $this->te('Status') ?></th>
                            <th scope="col"><?= $this->te('Erstellt von') ?></th>
                            <th scope="col"><?= $this->te('Erstellt am') ?></th>
                            <th scope="col"><?= $this->te('Zuletzt genutzt') ?></th>
                            <th scope="col"></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php
                        foreach ($tokens as $token):
                            $tokenScopes = json_decode($token['scopes'] ?? '["*"]', true) ?? ['*'];
                        ?>
                        <tr>
                            <td><strong><?= $this->e($token['name']) ?></strong></td>
                            <td>
                                <?php foreach ($tokenScopes as $s): ?>
                                <span style="display:inline-block;font-size:0.72rem;padding:0.15rem 0.4rem;
                                             border-radius:0.2rem;background:<?= $s === '*' ? '#dbeafe' : '#f0fdf4' ?>;
                                             color:<?= $s === '*' ? '#1e40af' : '#15803d' ?>;margin:0.1rem 0.1rem 0.1rem 0;">
                                    <?= $this->te(\OpenNIT\Api\ApiScopes::label((string)$s)) ?>
                                </span>
                                <?php endforeach; ?>
                            </td>
                            <td>
                                <?php if ($token['is_active']): ?>
                                    <span class="wp-badge wp-badge-active"><?= $this->te('Aktiv') ?></span>
                                <?php else: ?>
                                    <span class="wp-badge wp-badge-revoked"><?= $this->te('Widerrufen') ?></span>
                                <?php endif; ?>
                            </td>
                            <td style="color:var(--gray-500);"><?= $this->e($token['created_by_name'] ?? '—') ?></td>
                            <td style="font-size:0.8rem;color:var(--gray-400);"><?= substr($token['created_at'], 0, 10) ?></td>
                            <td style="font-size:0.8rem;color:var(--gray-400);"><?= $token['last_used_at'] ? substr($token['last_used_at'], 0, 16) : '—' ?></td>
                            <td>
                                <?php if ($token['is_active']): ?>
                                <form method="POST" action="/admin/api/tokens/<?= (int)$token['id'] ?>"
                                      onsubmit="return confirm('Token \'<?= $this->e(addslashes($token['name'])) ?>\' widerrufen?');">
                                    <input type="hidden" name="_method" value="DELETE">
                                    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token ?? '') ?>">
                                    <button type="submit" class="btn btn-outline-danger btn-sm"><?= $this->te('Widerrufen') ?></button>
                                </form>
                                <?php else: ?>
                                <form method="POST" action="/admin/api/tokens/<?= (int)$token['id'] ?>/delete"
                                      onsubmit="return confirm('Token \'<?= $this->e(addslashes($token['name'])) ?>\' endgültig löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.');">
                                    <input type="hidden" name="_method" value="DELETE">
                                    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token ?? '') ?>">
                                    <button type="submit" class="btn btn-outline-danger btn-sm"><?= $this->te('Löschen') ?></button>
                                </form>
                                <?php endif; ?>
                            </td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php endif; ?>
        </div>
    </div>
</div>

</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 3: WordPress Plugin                                         -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="wordpress-plugin" class="api-tab-content">

<!-- Direkter API-Zugriff -->
<div class="wp-section">
    <div class="wp-setup-info">
        <h3><?= $this->te('&#128736; Direkter Zugriff auf die API') ?></h3>
        <p style="margin:0 0 0.6rem;font-size:0.875rem;line-height:1.55;">
            <?= $this->t('Ein Bearer-Token allein gen&uuml;gt <strong>nicht</strong>: Bis auf die Agent-Endpunkte (IPAM-Scan, DNS-Import) verlangt jede Anfrage zus&auml;tzlich die Header <span class="wp-code">X-OpenNIT-Timestamp</span>, <span class="wp-code">X-OpenNIT-Nonce</span> und <span class="wp-code">X-OpenNIT-Signature</span>. Fehlen sie, antwortet die API mit <strong>401</strong> &ndash; auch bei g&uuml;ltigem Token.') ?>
        </p>
        <p style="margin:0 0 0.6rem;font-size:0.875rem;line-height:1.55;">
            <?= $this->te('Die Signatur bindet die im Aufruf behauptete Person an das Signing-Secret des Tokens. Ohne sie k&ouml;nnte jeder Token-Inhaber im Namen beliebiger Personen handeln, weshalb sie nicht abschaltbar ist.') ?>
        </p>
        <p style="margin:0;font-size:0.875rem;line-height:1.55;">
            <?= $this->t('Die genaue Bildung der Signatur samt Beispiel steht in der <strong>API-Dokumentation</strong> weiter unten (Abschnitt &bdquo;Signatur&ldquo;). Das Signing-Secret wird beim Anlegen eines Tokens einmalig angezeigt.') ?>
        </p>
    </div>
</div>

<!-- Plugin-Einrichtung -->
<div class="wp-section">
    <div class="wp-setup-info">
        <h3><?= $this->te('&#128274; WordPress Plugin Einrichtung') ?></h3>
        <dl>
            <dt><?= $this->te('API-Basis-URL') ?></dt>
            <dd><span class="wp-code"><?= $this->e($api_base_url) ?></span></dd>
            <dt><?= $this->te('Shortcodes') ?></dt>
            <dd>
                <span class="wp-code">[opennit_loan_devices]</span> <?= $this->te('– Leihgeräte + Meine Leihen') ?> &nbsp;
                <span class="wp-code">[opennit_assets]</span> <?= $this->te('– Meine Geräte') ?> &nbsp;
                <span class="wp-code">[opennit_ticket_form]</span> <?= $this->te('– Ticket erstellen') ?> &nbsp;
                <span class="wp-code">[opennit_my_tickets]</span> <?= $this->te('– Status meiner Tickets') ?> &nbsp;
                <span class="wp-code">[opennit_wiki]</span> <?= $this->te('– Wissensdatenbank') ?> &nbsp;
                <span class="wp-code">[opennit_wiki topic="<em>ID</em>"]</span> <?= $this->te('– Wiki mit festem Einstiegsthema') ?>
            </dd>
        </dl>
        <p class="wp-setup-note">
            <?= $this->t('Laden Sie das Plugin herunter, installieren Sie es in WordPress und tragen Sie API-URL und Token unter <em>Einstellungen &rarr; {menue}</em> ein.', ['menue' => $this->e($wp_menu_name ?? 'Lioriq-IT')]) ?>
        </p>
        <p class="wp-setup-note">
            <?= $this->t('Das Plugin steht als eigenst&auml;ndiges Werk unter <a href="https://www.gnu.org/licenses/gpl-2.0.html" target="_blank" rel="noopener">GPL&#8209;2.0&#8209;or&#8209;later</a> &ndash; nicht unter der AGPL&#8209;3.0 von OpenNIT. Der Lizenztext liegt dem Download als <span class="wp-code">LICENSE</span> bei und muss bei einer Weitergabe erhalten bleiben.') ?>
        </p>
    </div>
</div>

<!-- Plugin Download -->
<div class="wp-section">
    <div class="wp-card">
        <div class="wp-card-header">
            <h2>
                <svg class="wp-card-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
                <?= $this->te('Plugin Download') ?>
            </h2>
            <a href="/admin/api/plugin-download" class="btn btn-success">
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
                <?= $this->te('Plugin herunterladen (.zip)') ?>
            </a>
        </div>
        <div class="wp-card-body">
            <p style="margin: 0; font-size: 0.875rem; color: var(--gray-500);">
                <?= $this->te('Laden Sie das WordPress Plugin herunter und installieren Sie es in Ihrer WordPress-Instanz. Das Plugin stellt Shortcodes für Leihgeräte, Assets, Tickets und die Wissensdatenbank bereit.') ?>
            </p>
        </div>
    </div>
</div>

<!-- Plugin-Texte konfigurieren -->
<form method="POST" action="/admin/api/texts" enctype="multipart/form-data">
    <input type="hidden" name="_csrf" value="<?= $this->e($csrf_token ?? '') ?>">

    <!-- WordPress-Backend-Einstellungen -->
    <div class="wp-section">
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <svg class="wp-card-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>
                    <?= $this->te('WordPress-Backend') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <div class="wp-text-fields">
                    <div class="form-group">
                        <label for="wp_menu_name"><?= $this->te('Menüname im WordPress-Backend') ?></label>
                        <input class="form-control" type="text" id="wp_menu_name" name="wp_menu_name"
                               value="<?= $this->e($wp_menu_name ?? '') ?>"
                               placeholder="<?= $this->te('OpenNIT | Connector') ?>">
                        <p class="form-hint"><?= $this->t('Bezeichnung des Plugin-Eintrags unter <em>Einstellungen</em> in WordPress.') ?></p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="wp-grid-2" style="margin-bottom:1.25rem;">

        <!-- [opennit_loan_devices] + Buchungsregeln -->
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <svg class="wp-card-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-4 0v2"/><path d="M8 7V5a2 2 0 0 0-4 0v2"/></svg>
                    <code style="font-size:0.8rem;font-weight:400;">[opennit_loan_devices]</code> <?= $this->te('– Leihgeräte') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <div class="wp-text-fields">

                    <div class="form-group">
                        <label for="wp_text_my_loans"><?= $this->te('Überschrift „Meine Leihen"') ?></label>
                        <input class="form-control" type="text" id="wp_text_my_loans" name="wp_text_my_loans"
                               value="<?= $this->e($wp_text_my_loans ?? '') ?>"
                               placeholder="<?= $this->te('Meine Leihen') ?>">
                        <p class="form-hint"><?= $this->te('Abschnittsüberschrift für aktive eigene Leihen.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_cancel"><?= $this->te('Schaltfläche „Stornieren"') ?></label>
                        <input class="form-control" type="text" id="wp_text_cancel" name="wp_text_cancel"
                               value="<?= $this->e($wp_text_cancel ?? '') ?>"
                               placeholder="<?= $this->te('Stornieren') ?>">
                        <p class="form-hint"><?= $this->te('Schaltfläche zum Stornieren einer eigenen Reservierung.') ?></p>
                    </div>

                    <hr class="wp-divider">

                    <div class="form-group">
                        <label for="wp_text_loan_devices"><?= $this->te('Überschrift „Leihgeräte"') ?></label>
                        <input class="form-control" type="text" id="wp_text_loan_devices" name="wp_text_loan_devices"
                               value="<?= $this->e($wp_text_loan_devices ?? '') ?>"
                               placeholder="<?= $this->te('Leihgeräte') ?>">
                        <p class="form-hint"><?= $this->te('Abschnittsüberschrift für die Geräteliste.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_available"><?= $this->te('Status-Badge „Verfügbar"') ?></label>
                        <input class="form-control" type="text" id="wp_text_available" name="wp_text_available"
                               value="<?= $this->e($wp_text_available ?? '') ?>"
                               placeholder="<?= $this->te('Verfügbar') ?>">
                        <p class="form-hint"><?= $this->te('Anzeige, wenn ein Gerät verfügbar ist.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_request"><?= $this->te('Schaltfläche „Anfragen"') ?></label>
                        <input class="form-control" type="text" id="wp_text_request" name="wp_text_request"
                               value="<?= $this->e($wp_text_request ?? '') ?>"
                               placeholder="<?= $this->te('Anfragen') ?>">
                        <p class="form-hint"><?= $this->te('Schaltfläche zum Öffnen des Anfrageformulars.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_contact_it"><?= $this->te('Hinweis „IT-Team kontaktieren"') ?></label>
                        <input class="form-control" type="text" id="wp_text_contact_it" name="wp_text_contact_it"
                               value="<?= $this->e($wp_text_contact_it ?? '') ?>"
                               placeholder="<?= $this->te('Bitte IT-Team direkt kontaktieren') ?>">
                        <p class="form-hint"><?= $this->te('Wird angezeigt, wenn ein Gerät nicht online anfragbar ist.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_reservation_success"><?= $this->te('Bestätigungstext nach') ?> <strong><?= $this->te('Direktreservierung') ?></strong></label>
                        <textarea class="form-control" id="wp_text_reservation_success" name="wp_text_reservation_success"
                                  rows="3"
                                  placeholder="<?= $this->te('Erfolgreich reserviert. Zur Abholung vereinbaren Sie bitte mit der IT-Abteilung einen Termin.') ?>"><?= $this->e($wp_text_reservation_success ?? '') ?></textarea>
                        <p class="form-hint"><?= $this->t('Wird angezeigt, wenn das Gerät <strong>direkt fest reserviert</strong> wird (Freigabe deaktiviert).') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_request_success"><?= $this->t('Bestätigungstext nach <strong>Anfrage</strong> (Freigabe nötig)') ?></label>
                        <textarea class="form-control" id="wp_text_request_success" name="wp_text_request_success"
                                  rows="3"
                                  placeholder="<?= $this->te('Ihre Anfrage wurde übermittelt. Die Freigabe erfolgt durch das IT-Team – Sie erhalten anschließend eine E-Mail.') ?>"><?= $this->e($wp_text_request_success ?? '') ?></textarea>
                        <p class="form-hint"><?= $this->t('Wird angezeigt, wenn die Anfrage <strong>erst vom IT-Team bestätigt</strong> werden muss (Freigabe aktiv – siehe unten). Hier sollte <em>nicht</em> „reserviert" stehen, sondern z. B. „angefragt – Freigabe folgt".') ?></p>
                    </div>

                    <hr class="wp-divider">
                    <p class="wp-section-label" style="margin:0 0 0.75rem;"><?= $this->te('Buchungsregeln') ?></p>

                    <div class="form-group">
                        <label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;font-weight:500;">
                            <input type="checkbox" name="wp_loan_require_approval" value="1"
                                   <?= ($wp_loan_require_approval ?? '1') !== '0' ? 'checked' : '' ?>
                                   style="width:auto;margin:0;">
                            <?= $this->te('Anfragen müssen vom IT-Team bestätigt werden') ?>
                        </label>
                        <p class="form-hint">
                            <?= $this->t('Aktiv (empfohlen): Eine Anfrage über die API/das Portal wird als <strong>offene Anfrage</strong> angelegt und muss im Backend bestätigt werden. Inaktiv: Anfragen werden <strong>sofort fest reserviert</strong>. Einzelne API-Aufrufe können dies über den Body-Parameter <code>auto_reserve</code> übersteuern.') ?>
                        </p>
                    </div>

                    <div class="form-group">
                        <label for="wp_loan_max_advance_days"><?= $this->te('Max. Vorlaufzeit (wie weit im Voraus)') ?></label>
                        <select class="form-control" id="wp_loan_max_advance_days" name="wp_loan_max_advance_days"
                                style="padding:0.45rem 0.75rem;border:1px solid var(--gray-300);border-radius:0.3rem;font-size:0.875rem;max-width:280px;">
                            <?php
                            $advanceOptions = [
                                7   => '1 Woche',
                                14  => '2 Wochen',
                                30  => '1 Monat',
                                60  => '2 Monate',
                                90  => '3 Monate',
                                180 => '6 Monate',
                                365 => '12 Monate',
                            ];
                            $currentAdvance = (int)($wp_loan_max_advance_days ?? 90);
                            foreach ($advanceOptions as $days => $label):
                            ?>
                            <option value="<?= $days ?>"<?= $currentAdvance === $days ? ' selected' : '' ?>><?= $label ?></option>
                            <?php endforeach; ?>
                        </select>
                        <p class="form-hint"><?= $this->te('Wie weit in der Zukunft darf ein Nutzer ein Leihgerät reservieren?') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_loan_max_duration_days"><?= $this->te('Max. Ausleihdauer') ?></label>
                        <select class="form-control" id="wp_loan_max_duration_days" name="wp_loan_max_duration_days"
                                style="padding:0.45rem 0.75rem;border:1px solid var(--gray-300);border-radius:0.3rem;font-size:0.875rem;max-width:280px;">
                            <?php
                            $durationOptions = [
                                1  => '1 Tag',
                                3  => '3 Tage',
                                5  => '5 Tage',
                                7  => '1 Woche',
                                14 => '2 Wochen',
                                21 => '3 Wochen',
                                30 => '1 Monat',
                                60 => '2 Monate',
                                90 => '3 Monate',
                            ];
                            $currentDuration = (int)($wp_loan_max_duration_days ?? 14);
                            foreach ($durationOptions as $days => $label):
                            ?>
                            <option value="<?= $days ?>"<?= $currentDuration === $days ? ' selected' : '' ?>><?= $label ?></option>
                            <?php endforeach; ?>
                        </select>
                        <p class="form-hint"><?= $this->te('Wie lange darf ein einzelner Leihvorgang maximal dauern?') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_error_too_far_ahead"><?= $this->te('Fehlermeldung: Datum zu weit in der Zukunft') ?></label>
                        <textarea class="form-control" id="wp_text_error_too_far_ahead" name="wp_text_error_too_far_ahead"
                                  rows="2"
                                  placeholder="<?= $this->te('Das gewählte Datum liegt zu weit in der Zukunft. Bitte wählen Sie einen früheren Zeitraum.') ?>"><?= $this->e($wp_text_error_too_far_ahead ?? '') ?></textarea>
                        <p class="form-hint"><?= $this->te('Wird angezeigt, wenn der Startzeitpunkt die erlaubte Vorlaufzeit überschreitet.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_error_too_long"><?= $this->te('Fehlermeldung: Zeitraum zu lang') ?></label>
                        <textarea class="form-control" id="wp_text_error_too_long" name="wp_text_error_too_long"
                                  rows="2"
                                  placeholder="<?= $this->te('Der gewählte Zeitraum ist zu lang. Bitte wählen Sie einen kürzeren Zeitraum.') ?>"><?= $this->e($wp_text_error_too_long ?? '') ?></textarea>
                        <p class="form-hint"><?= $this->te('Wird angezeigt, wenn die gewählte Dauer die maximale Ausleihdauer überschreitet.') ?></p>
                    </div>

                </div>
            </div>
        </div>

        <!-- [opennit_assets] -->
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <svg class="wp-card-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
                    <code style="font-size:0.8rem;font-weight:400;">[opennit_assets]</code> <?= $this->te('– Meine Geräte') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <div class="wp-text-fields">

                    <div class="form-group">
                        <label for="wp_text_no_devices"><?= $this->te('Meldung bei leerer Geräteliste') ?></label>
                        <input class="form-control" type="text" id="wp_text_no_devices" name="wp_text_no_devices"
                               value="<?= $this->e($wp_text_no_devices ?? '') ?>"
                               placeholder="<?= $this->te('Ihnen sind aktuell keine Geräte zugewiesen.') ?>">
                        <p class="form-hint"><?= $this->te('Angezeigt, wenn dem Nutzer keine Geräte zugewiesen sind.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_text_email_hint"><?= $this->te('Hinweis zur E-Mail-Adresse') ?></label>
                        <textarea class="form-control" id="wp_text_email_hint" name="wp_text_email_hint"
                                  rows="3"
                                  placeholder="<?= $this->te('Hinweis: Ihre E-Mail-Adresse muss im IT-System hinterlegt sein, damit Geräte hier angezeigt werden.') ?>"><?= $this->e($wp_text_email_hint ?? '') ?></textarea>
                        <p class="form-hint"><?= $this->te('Hilfetext unter der leeren Geräteliste.') ?></p>
                    </div>

                </div>
            </div>
        </div>

    </div><!-- /wp-grid-2 -->

    <!-- [opennit_ticket_form] & [opennit_my_tickets] -->
    <div class="wp-section">
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <span class="wp-card-icon">🎫</span>
                    <code style="font-size:0.8rem;font-weight:400;">[opennit_ticket_form]</code> &amp; <code style="font-size:0.8rem;font-weight:400;">[opennit_my_tickets]</code> <?= $this->te('– Ticket-Formular') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <div class="wp-text-fields">

                    <div class="form-group">
                        <label for="wp_ticket_form_title"><?= $this->te('Formular-Überschrift') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_form_title" name="wp_ticket_form_title"
                               value="<?= $this->e($wp_ticket_form_title ?? '') ?>"
                               placeholder="<?= $this->te('IT-Ticket erstellen') ?>">
                        <p class="form-hint"><?= $this->te('Überschrift über dem Ticket-Formular.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_submit_label"><?= $this->te('Absenden-Button') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_submit_label" name="wp_ticket_submit_label"
                               value="<?= $this->e($wp_ticket_submit_label ?? '') ?>"
                               placeholder="<?= $this->te('Ticket absenden') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_success_message"><?= $this->te('Erfolgsmeldung nach Absenden') ?></label>
                        <textarea class="form-control" id="wp_ticket_success_message" name="wp_ticket_success_message"
                                  rows="2"
                                  placeholder="<?= $this->te('Ihr Ticket wurde erfolgreich übermittelt. Das IT-Team wird sich darum kümmern.') ?>"><?= $this->e($wp_ticket_success_message ?? '') ?></textarea>
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_login_required"><?= $this->te('Hinweis: Login erforderlich') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_login_required" name="wp_ticket_login_required"
                               value="<?= $this->e($wp_ticket_login_required ?? '') ?>"
                               placeholder="<?= $this->te('Bitte melden Sie sich an, um ein Ticket zu erstellen.') ?>">
                        <p class="form-hint"><?= $this->te('Wird angezeigt, wenn der Nutzer nicht eingeloggt ist.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_subject_placeholder"><?= $this->te('Platzhalter: Betreff-Feld') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_subject_placeholder" name="wp_ticket_subject_placeholder"
                               value="<?= $this->e($wp_ticket_subject_placeholder ?? '') ?>"
                               placeholder="<?= $this->te('Kurze Beschreibung des Problems…') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_desc_placeholder"><?= $this->te('Platzhalter: Beschreibungsfeld') ?></label>
                        <textarea class="form-control" id="wp_ticket_desc_placeholder" name="wp_ticket_desc_placeholder"
                                  rows="2"
                                  placeholder="<?= $this->te('Beschreiben Sie das Problem so genau wie möglich…') ?>"><?= $this->e($wp_ticket_desc_placeholder ?? '') ?></textarea>
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_attachment_hint"><?= $this->te('Hinweis unter dem Dateianhang-Feld') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_attachment_hint" name="wp_ticket_attachment_hint"
                               value="<?= $this->e($wp_ticket_attachment_hint ?? '') ?>"
                               placeholder="<?= $this->te('Optional: Screenshot oder Fehlerbeschreibung anhängen (max. 10 MB, Bild oder PDF).') ?>">
                    </div>

                    <hr class="wp-divider">

                    <div class="form-group">
                        <label for="wp_ticket_my_title"><?= $this->te('Überschrift „Meine Tickets"') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_my_title" name="wp_ticket_my_title"
                               value="<?= $this->e($wp_ticket_my_title ?? '') ?>"
                               placeholder="<?= $this->te('Meine Tickets') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_ticket_no_tickets"><?= $this->te('Hinweis wenn keine Tickets vorhanden') ?></label>
                        <input class="form-control" type="text" id="wp_ticket_no_tickets" name="wp_ticket_no_tickets"
                               value="<?= $this->e($wp_ticket_no_tickets ?? '') ?>"
                               placeholder="<?= $this->te('Sie haben noch keine Tickets erstellt.') ?>">
                    </div>

                </div>
            </div>
        </div>
    </div>

    <!-- [opennit_wiki] -->
    <div class="wp-section">
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <span class="wp-card-icon">📚</span>
                    <code style="font-size:0.8rem;font-weight:400;">[opennit_wiki]</code> <?= $this->te('– Wissensdatenbank') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <div class="wp-text-fields">

                    <div class="form-group">
                        <label for="wp_wiki_empty"><?= $this->te('Hinweis wenn keine Themen verfügbar') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_empty" name="wp_wiki_empty"
                               value="<?= $this->e($wp_wiki_empty ?? '') ?>"
                               placeholder="<?= $this->te('Keine Themen verfügbar.') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_wiki_no_pages"><?= $this->te('Hinweis wenn Thema noch keine Seiten hat') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_no_pages" name="wp_wiki_no_pages"
                               value="<?= $this->e($wp_wiki_no_pages ?? '') ?>"
                               placeholder="<?= $this->te('Dieses Thema enthält noch keine Seiten.') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_wiki_subtopics_label"><?= $this->te('Abschnittsüberschrift „Unterthemen"') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_subtopics_label" name="wp_wiki_subtopics_label"
                               value="<?= $this->e($wp_wiki_subtopics_label ?? '') ?>"
                               placeholder="<?= $this->te('Unterthemen') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_wiki_pages_label"><?= $this->te('Abschnittsüberschrift „Seiten"') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_pages_label" name="wp_wiki_pages_label"
                               value="<?= $this->e($wp_wiki_pages_label ?? '') ?>"
                               placeholder="<?= $this->te('Seiten') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_wiki_toc_label"><?= $this->te('Inhaltsverzeichnis-Überschrift') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_toc_label" name="wp_wiki_toc_label"
                               value="<?= $this->e($wp_wiki_toc_label ?? '') ?>"
                               placeholder="<?= $this->te('Inhalt') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_wiki_back_label"><?= $this->te('Schaltfläche „Zurück zur Übersicht"') ?></label>
                        <input class="form-control" type="text" id="wp_wiki_back_label" name="wp_wiki_back_label"
                               value="<?= $this->e($wp_wiki_back_label ?? '') ?>"
                               placeholder="<?= $this->te('Übersicht') ?>">
                    </div>

                </div>
                <div style="margin-top:1rem;padding:0.75rem 1rem;background:var(--blue-50);border:1px solid var(--blue-200);border-radius:0.4rem;font-size:0.82rem;color:var(--blue-900);">
                    <?= $this->t('<strong>Attribut <code>topic="ID"</code>:</strong> Mit <code>[opennit_wiki topic="5"]</code> wird ein festes Wurzelthema gesetzt. Die globale Themenübersicht entfällt; der Shortcode zeigt direkt Unterthemen und Seiten des gewählten Themas. Der Zurück-Button ist auf der Wurzelebene ausgeblendet und erscheint erst in Unterthemen (führt dann maximal zum konfigurierten Wurzelthema zurück).') ?>
                </div>
                <div style="margin-top:0.5rem;padding:0.75rem 1rem;background:var(--green-50);border:1px solid var(--green-200);border-radius:0.4rem;font-size:0.82rem;color:var(--green-900);">
                    <?= $this->t('<strong>Cache:</strong> Inhalte werden 24 Stunden gecacht. Cache leeren im WordPress-Backend unter <em>Einstellungen → OpenNIT | Connector → Wiki-Cache leeren</em>.') ?>
                </div>
            </div>
        </div>
    </div>

    <!-- [opennit_assistant] – KI-Assistent (Website-Bot) -->
    <div class="wp-section">
        <div class="wp-card">
            <div class="wp-card-header">
                <h2>
                    <span class="wp-card-icon">🤖</span>
                    <code style="font-size:0.8rem;font-weight:400;">[opennit_assistant]</code> <?= $this->te('– KI-Assistent (Website-Bot)') ?>
                </h2>
            </div>
            <div class="wp-card-body">
                <p style="margin:0 0 1rem;font-size:0.82rem;color:var(--gray-500);">
                    <?= $this->t('Diese Einstellungen werden vom WordPress-Plugin automatisch übernommen (Konfigurations-Cache, Aktualisierung innerhalb weniger Minuten). Der Bot muss zusätzlich unter <a href="/admin/settings/ai-chat"><em>Einstellungen → KI-Chat</em></a> aktiviert sein (Karte „Website-Bot"), und der API-Token benötigt den Scope <code>assistant</code> (für die Ticket-Erstellung zusätzlich <code>tickets</code>).') ?>
                </p>
                <div class="wp-text-fields">

                    <div class="form-group">
                        <label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;font-weight:500;">
                            <input type="checkbox" name="wp_assistant_bubble" value="1"
                                   <?= ($wp_assistant_bubble ?? '0') === '1' ? 'checked' : '' ?>
                                   style="width:auto;margin:0;">
                            <?= $this->te('Schwebende Chat-Bubble auf allen Seiten anzeigen') ?>
                        </label>
                        <p class="form-hint"><?= $this->t('Alternativ lässt sich der Assistent per Shortcode <code>[opennit_assistant]</code> einbetten.') ?></p>
                    </div>

                    <div class="form-group">
                        <label style="display:flex;align-items:center;gap:0.5rem;cursor:pointer;font-weight:500;">
                            <input type="checkbox" name="wp_assistant_logged_in_only" value="1"
                                   <?= ($wp_assistant_logged_in_only ?? '0') === '1' ? 'checked' : '' ?>
                                   style="width:auto;margin:0;">
                            <?= $this->te('Bubble nur für angemeldete Nutzer anzeigen') ?>
                        </label>
                        <p class="form-hint"><?= $this->te('Betrifft nur die schwebende Bubble, nicht den Shortcode.') ?></p>
                    </div>

                    <div class="form-group">
                        <label for="wp_assistant_title"><?= $this->te('Titel des Assistenten') ?></label>
                        <input class="form-control" type="text" id="wp_assistant_title" name="wp_assistant_title"
                               value="<?= $this->e($wp_assistant_title ?? '') ?>"
                               placeholder="<?= $this->te('IT-Assistent') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_assistant_welcome"><?= $this->te('Begrüßungstext') ?></label>
                        <input class="form-control" type="text" id="wp_assistant_welcome" name="wp_assistant_welcome"
                               value="<?= $this->e($wp_assistant_welcome ?? '') ?>"
                               placeholder="<?= $this->te('Hallo! Ich helfe bei einfachen PC-Fragen und unserer Dokumentation.') ?>">
                    </div>

                    <div class="form-group">
                        <label for="wp_assistant_context"><?= $this->te('Zusätzlicher Kontext für den Assistenten') ?></label>
                        <textarea class="form-control" id="wp_assistant_context" name="wp_assistant_context" rows="4"
                                  placeholder="<?= $this->te('z. B. Öffnungszeiten des IT-Supports, Standort-Besonderheiten, Hinweise…') ?>"><?= $this->e($wp_assistant_context ?? '') ?></textarea>
                        <p class="form-hint">
                            <?= $this->t('Wird dem Assistenten bei jeder Anfrage als zusätzlicher Hintergrund mitgegeben (max. 4000 Zeichen). Die E-Mail angemeldeter Nutzer wird dem Bot automatisch als Kontext übergeben. Besitzt der Token den Scope <code>tickets</code>, kann der Bot im Gespräch ein Support-Ticket erstellen.') ?>
                        </p>
                    </div>

                    <div class="form-group">
                        <label for="wp_assistant_icon"><?= $this->te('Icon der Chat-Bubble') ?></label>
                        <?php if (!empty($wp_assistant_icon)): ?>
                            <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;">
                                <img src="<?= $this->e($wp_assistant_icon) ?>" alt="<?= $this->te('Aktuelles Bubble-Icon') ?>"
                                     style="width:44px;height:44px;object-fit:contain;border:1px solid var(--gray-200);border-radius:8px;background:var(--gray-50);padding:4px;">
                                <label style="display:flex;align-items:center;gap:0.4rem;font-weight:500;cursor:pointer;font-size:0.85rem;">
                                    <input type="checkbox" name="wp_assistant_icon_remove" value="1" style="width:auto;margin:0;">
                                    <?= $this->te('Aktuelles Icon entfernen') ?>
                                </label>
                            </div>
                        <?php endif; ?>
                        <input class="form-control" type="file" id="wp_assistant_icon" name="wp_assistant_icon"
                               accept=".png,.jpg,.jpeg,.gif,.webp,image/png,image/jpeg,image/gif,image/webp">
                        <p class="form-hint">
                            <?= $this->te('Wird auf der schwebenden Chat-Bubble angezeigt (statt der Standard-Sprechblase). PNG, JPG, GIF oder WebP, max. 1 MB. Empfohlen: quadratisch, ca. 64×64 px.') ?>
                        </p>
                    </div>

                </div>
            </div>
        </div>
    </div>

    <div style="display:flex;align-items:center;gap:1rem;">
        <button type="submit" class="btn btn-primary">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
            <?= $this->te('Texte speichern') ?>
        </button>
        <span style="font-size:0.8rem;color:var(--gray-400);"><?= $this->te('Leere Felder werden auf den Standardtext zurückgesetzt.') ?></span>
    </div>

</form>

</div>

<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TAB 4: Zugriffskontrolle                                        -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="access-control" class="api-tab-content">

    <div class="wp-card">
        <div class="wp-card-header">
            <h2>
                <svg class="wp-card-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
                <?= $this->te('Topic-Whitelist') ?>
            </h2>
        </div>
        <div class="wp-card-body">
            <p style="margin:0 0 1rem;font-size:0.875rem;color:var(--gray-500);">
                <?= $this->t('Nur freigegebene Topics werden über den Endpunkt <code style="background:var(--gray-100);padding:0.1rem 0.35rem;border-radius:0.2rem;font-size:0.8rem;">/api/ex/topics/{id}/pages</code> ausgeliefert. <strong style="color:var(--red-600);">Deny-by-default:</strong> Kein Haken = kein Zugriff.') ?>
            </p>

            <?php if (empty($all_topics)): ?>
                <p class="wp-empty"><?= $this->te('Keine Topics vorhanden.') ?></p>
            <?php else: ?>
            <div id="whitelist-feedback" style="display:none;margin-bottom:0.75rem;padding:0.5rem 0.9rem;border-radius:0.3rem;font-size:0.82rem;"></div>
            <div style="overflow-x:auto;">
            <table class="wp-table" id="whitelist-table">
                <thead>
                    <tr>
                        <th scope="col" style="width:40px;">API</th>
                        <th scope="col"><?= $this->te('Topic') ?></th>
                        <th scope="col" style="width:60px;"><?= $this->te('Ebene') ?></th>
                    </tr>
                </thead>
                <tbody>
                <?php foreach ($all_topics as $topic): ?>
                    <tr data-topic-id="<?= (int)$topic['id'] ?>">
                        <td>
                            <label class="whitelist-toggle" style="cursor:pointer;display:inline-flex;align-items:center;">
                                <input type="checkbox"
                                       class="whitelist-checkbox"
                                       data-topic-id="<?= (int)$topic['id'] ?>"
                                       <?= $topic['api_enabled'] ? 'checked' : '' ?>
                                       style="width:1rem;height:1rem;cursor:pointer;">
                            </label>
                        </td>
                        <td>
                            <?php if ($topic['depth'] > 0): ?>
                            <span style="display:inline-block;width:<?= (int)$topic['depth'] * 20 ?>px;"></span>
                            <span style="color:var(--gray-300);margin-right:0.35rem;">└</span>
                            <?php endif; ?>
                            <?= htmlspecialchars($topic['name']) ?>
                        </td>
                        <td style="color:var(--gray-400);font-size:0.78rem;"><?= (int)$topic['depth'] === 0 ? 'Root' : 'Ebene ' . (int)$topic['depth'] ?></td>
                    </tr>
                <?php endforeach; ?>
                </tbody>
            </table>
            </div><!-- /overflow-x:auto -->
            <?php endif; ?>
        </div>
    </div>

</div>

<script>
var _swaggerInitialized = false;

function initSwaggerUI() {
    if (_swaggerInitialized || typeof SwaggerUIBundle === 'undefined') return;
    _swaggerInitialized = true;
    SwaggerUIBundle({
        spec:            <?= $openapi_spec_json ?>,
        dom_id:          '#swagger-ui-container',
        presets:         [SwaggerUIBundle.presets.apis],
        layout:          'BaseLayout',
        deepLinking:     false,
        withCredentials: true,
        tryItOutEnabled: true,
        supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
    });
}

document.querySelectorAll('.settings-tab[data-tab]').forEach(function(tab) {
    tab.addEventListener('click', function(e) {
        e.preventDefault();
        var tabName = this.getAttribute('data-tab');
        document.querySelectorAll('.api-tab-content').forEach(function(c) { c.classList.remove('active'); });
        document.querySelectorAll('.settings-tab[data-tab]').forEach(function(t) { t.classList.remove('active'); });
        document.getElementById(tabName).classList.add('active');
        this.classList.add('active');
        if (tabName === 'api-docs') {
            if (typeof SwaggerUIBundle !== 'undefined') {
                initSwaggerUI();
            } else {
                var s = document.createElement('script');
                s.src = '/admin/api/swagger-ui.js';
                s.onload = initSwaggerUI;
                document.head.appendChild(s);
            }
        }
    });
});

// Whitelist-Toggle
document.querySelectorAll('.whitelist-checkbox').forEach(function(cb) {
    cb.addEventListener('change', function() {
        var topicId = this.getAttribute('data-topic-id');
        var checkbox = this;
        checkbox.disabled = true;

        var csrfToken = document.querySelector('meta[name="csrf-token"]');
        var headers = {'Content-Type': 'application/x-www-form-urlencoded'};
        if (csrfToken) {
            headers['X-CSRF-Token'] = csrfToken.getAttribute('content');
        }

        fetch('/admin/api/whitelist/toggle', {
            method: 'POST',
            headers: headers,
            body: 'topic_id=' + encodeURIComponent(topicId)
        })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            checkbox.disabled = false;
            if (data.ok) {
                checkbox.checked = data.enabled;
                var fb = document.getElementById('whitelist-feedback');
                fb.textContent = data.enabled ? 'Topic freigeschaltet.' : 'Topic gesperrt.';
                fb.style.display = 'block';
                fb.style.background = data.enabled ? '#dcfce7' : '#fee2e2';
                fb.style.color = data.enabled ? '#15803d' : '#b91c1c';
                setTimeout(function() { fb.style.display = 'none'; }, 2500);
            } else {
                checkbox.checked = !checkbox.checked;
                alert(data.error || 'Fehler beim Speichern.');
            }
        })
        .catch(function() {
            checkbox.disabled = false;
            checkbox.checked = !checkbox.checked;
            alert('Netzwerkfehler. Bitte erneut versuchen.');
        });
    });
});
</script>