Quelltext des aktuellen Standes
<?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>⚠ 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('🔑 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') ?>">⚠</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('🔑 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('🛠 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ügt <strong>nicht</strong>: Bis auf die Agent-Endpunkte (IPAM-Scan, DNS-Import) verlangt jede Anfrage zusä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> – auch bei gü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ö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 „Signatur“). 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('🔒 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') ?>
<span class="wp-code">[opennit_assets]</span> <?= $this->te('– Meine Geräte') ?>
<span class="wp-code">[opennit_ticket_form]</span> <?= $this->te('– Ticket erstellen') ?>
<span class="wp-code">[opennit_my_tickets]</span> <?= $this->te('– Status meiner Tickets') ?>
<span class="wp-code">[opennit_wiki]</span> <?= $this->te('– Wissensdatenbank') ?>
<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 → {menue}</em> ein.', ['menue' => $this->e($wp_menu_name ?? 'Lioriq-IT')]) ?>
</p>
<p class="wp-setup-note">
<?= $this->t('Das Plugin steht als eigenständiges Werk unter <a href="https://www.gnu.org/licenses/gpl-2.0.html" target="_blank" rel="noopener">GPL‑2.0‑or‑later</a> – nicht unter der AGPL‑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> & <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>