Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => $connection['name'],
'current_admin_page' => 'api-auto',
'breadcrumbs' => [
['label' => 'API-Auto', 'url' => '/admin/api-auto'],
['label' => $connection['name']],
],
]); ?>
<?php $c = $connection; $cid = (int)$c['id']; $previewOk = !empty($c['preview_ok']); ?>
<div class="settings-page">
<div id="apiAutoMsg"></div>
<!-- ── KI-Mapping & Ausführung ─────────────────────────────────────── -->
<div class="card card-outline card-info mb-3">
<div class="card-header"><h3 class="card-title"><?= $this->te('KI-Mapping & Ausführung') ?></h3></div>
<div class="card-body">
<p class="text-muted">
<?= $this->te('Status:') ?>
<span class="badge badge-<?= $c['status'] === 'ready' ? 'success' : 'secondary' ?>">
<?= $c['status'] === 'ready' ? 'bereit' : 'Entwurf' ?>
</span>
<?= $this->te('· Dry-Run:') ?>
<span class="badge badge-<?= $previewOk ? 'success' : 'warning' ?>" id="previewBadge">
<?= $previewOk ? 'geprüft' : 'ausstehend' ?>
</span>
<?php if ($previewOk && !empty($c['last_preview_at'])): ?><small class="text-muted">(<?= $this->e($c['last_preview_at']) ?>)</small><?php endif; ?>
<?= $this->te('· Ziel:') ?> <strong><?= $this->e($target ? $target->label() : ($c['target_entity'] ?: '—')) ?></strong>
<?= $this->te('· Richtung:') ?> <strong><?= ($c['direction'] ?? 'import') === 'export' ? 'Export' : 'Import' ?></strong>
<?php if (($c['sync_mode'] ?? 'full') === 'incremental'): ?>
· <span class="badge badge-info"><?= $this->te('inkrementell') ?></span>
<?php if (!empty($c['last_synced_at'])): ?><small class="text-muted"><?= $this->t('(zuletzt') ?>: <?= $this->e($c['last_synced_at']) ?>)</small><?php endif; ?>
<?php endif; ?>
</p>
<?php if (!$previewOk): ?>
<div class="alert alert-warning py-2">
<i class="fas fa-shield-alt"></i> <?= $this->t('Diese Verbindung muss vor dem produktiven Lauf einmal erfolgreich per <strong>Vorschau (Dry-Run)</strong> auf der gespeicherten Konfiguration geprüft werden. Erst danach lässt sie sich ausführen oder regelmäßig einplanen.') ?>
</div>
<?php endif; ?>
<div id="testResult" class="mb-2"></div>
<?php if ($can_create): ?>
<button type="button" class="btn btn-info btn-sm" id="btnAnalyze">
<i class="fas fa-robot"></i> <?= $this->te('KI analysieren lassen') ?>
</button>
<?php endif; ?>
<?php if ($can_run): ?>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnTest">
<i class="fas fa-plug"></i> <?= $this->te('Verbindung testen') ?>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnPreview">
<i class="fas fa-eye"></i> <?= $this->te('Vorschau (Dry-Run)') ?>
</button>
<button type="button" class="btn btn-primary btn-sm" id="btnRun"
<?= ($c['status'] !== 'ready' || !$previewOk) ? 'disabled' : '' ?>
title="<?= $c['status'] !== 'ready' ? 'Mapping fehlt' : (!$previewOk ? 'Zuerst Vorschau (Dry-Run) durchführen' : '') ?>">
<i class="fas fa-play"></i> <?= ($c['direction'] ?? 'import') === 'export' ? 'Jetzt senden' : 'Jetzt ausführen' ?>
</button>
<?php endif; ?>
<div id="suggestBox" class="mt-3" style="display:none;">
<div class="alert alert-info" id="suggestSummary"></div>
<p class="small text-muted mb-1" id="suggestMeta"></p>
<pre id="suggestMapping" style="max-height:320px;overflow:auto;background:var(--color-gray-900);color:var(--gray-300);padding:1rem;border-radius:4px;font-size:12px;"></pre>
<?php if ($can_create): ?>
<p class="small text-muted mb-2" id="authHint" style="display:none;">
<i class="fas fa-key"></i> <?= $this->t('Diese API benötigt Zugangsdaten – trage sie unten im Abschnitt <strong>„Zugangsdaten"</strong> ein. Beim Übernehmen werden sie automatisch mitgespeichert.') ?>
</p>
<button type="button" class="btn btn-success btn-sm" id="btnConfirm">
<i class="fas fa-check"></i> <?= $this->te('Übernehmen & bereitstellen') ?>
</button>
<?php endif; ?>
</div>
<details class="mt-3"<?= empty($mapping) ? ' open' : '' ?>>
<summary class="text-muted"><?= $this->te('Mapping ansehen und von Hand bearbeiten') ?></summary>
<?php if ($can_create): ?>
<p class="small text-muted" style="margin:.6rem 0 .4rem;">
<?= $this->t('Hier lässt sich die Zuordnung ohne KI schreiben oder ein Vorschlag nachbessern. Geprüft wird gegen die freigegebenen Felder des Ziels; was nicht durchkommt, wird benannt. Mit <code>"write": "if_empty"</code> an einem Feld füllt der Import es nur, solange im Bestand nichts steht – so überschreibt kein Lauf eine Korrektur von Hand.') ?>
</p>
<textarea id="mappingEditor" class="form-control form-control-sm" rows="16"
spellcheck="false" style="font-family:monospace;font-size:12px;"><?= $this->e(json_encode($mapping ?: ['request' => ['path' => ''], 'records_path' => '', 'field_map' => new stdClass()], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></textarea>
<div id="mappingHinweise" class="mt-2" style="display:none;"></div>
<div class="mt-2" style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
<button type="button" class="btn btn-sm btn-primary" id="btnSaveMapping">
<i class="fas fa-save"></i> <?= $this->te('Mapping speichern') ?>
</button>
<span class="small text-muted"><?= $this->te('Nach dem Speichern ist erneut eine Vorschau nötig.') ?></span>
</div>
<?php else: ?>
<pre style="max-height:320px;overflow:auto;background:var(--color-gray-900);color:var(--gray-300);padding:1rem;border-radius:4px;font-size:12px;"><?= $this->e(json_encode($mapping, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
<?php endif; ?>
</details>
<div id="previewBox" class="mt-3" style="display:none;">
<h6><?= $this->te('Vorschau (es wird nichts geschrieben)') ?></h6>
<div id="previewWirkung" class="mb-2"></div>
<details>
<summary class="text-muted"><?= $this->te('Geholte Daten ansehen') ?></summary>
<pre id="previewContent" style="max-height:320px;overflow:auto;background:var(--color-gray-50);padding:1rem;border-radius:4px;font-size:12px;"></pre>
</details>
</div>
</div>
</div>
<!-- ── Zugangsdaten (einzige Eingabestelle) ─────────────────────────── -->
<?php if ($can_create): ?>
<div class="card card-outline card-warning mb-3" id="credCard" style="<?= ($c['auth_type'] ?? 'none') !== 'none' ? '' : 'display:none;' ?>">
<div class="card-header"><h3 class="card-title"><i class="fas fa-key"></i> <?= $this->te('Zugangsdaten') ?></h3></div>
<div class="card-body">
<p class="small text-muted">
<?= $this->t('Die Zugangsdaten sind die einzige Angabe, die nicht aus der API-Beschreibung kommen kann. Die KI-Analyse erkennt den passenden Auth-Typ und – beim Login-Flow – die nötigen Felder automatisch. <strong>Leer lassen = unverändert.</strong>') ?>
</p>
<form id="credForm" onsubmit="return false;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="form-group">
<label for="authType"><?= $this->te('Auth-Typ') ?></label>
<select name="auth_type" class="form-control" id="authType" style="max-width:320px;">
<?php foreach (['none' => 'Keine', 'api_key' => 'API-Key', 'bearer' => 'Bearer-Token', 'basic' => 'Basic-Auth', 'login' => 'Login (Token-Exchange)'] as $k => $lbl): ?>
<option value="<?= $k ?>" <?= ($c['auth_type'] ?? 'none') === $k ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="auth-fields" data-auth="api_key" style="display:none;">
<div class="form-group">
<label for="f-ddb0-auth_key_in"><?= $this->te('API-Key Position') ?></label>
<select id="f-ddb0-auth_key_in" name="auth_key_in" class="form-control" style="max-width:320px;">
<option value="header"><?= $this->te('Header') ?></option>
<option value="query"><?= $this->te('Query-Parameter') ?></option>
</select>
</div>
<div class="form-group"><label for="f-ddb0-auth_key_name"><?= $this->te('Name') ?></label><input id="f-ddb0-auth_key_name" type="text" name="auth_key_name" class="form-control" value="X-API-Key" style="max-width:320px;"></div>
<div class="form-group"><label for="f-ddb0-auth_key_value"><?= $this->te('Wert') ?></label><input id="f-ddb0-auth_key_value" type="password" name="auth_key_value" class="form-control" autocomplete="new-password" style="max-width:480px;"></div>
</div>
<div class="auth-fields" data-auth="bearer" style="display:none;">
<div class="form-group"><label for="f-ddb0-auth_token"><?= $this->te('Token') ?></label><input id="f-ddb0-auth_token" type="password" name="auth_token" class="form-control" autocomplete="new-password" style="max-width:480px;"></div>
</div>
<div class="auth-fields" data-auth="basic" style="display:none;">
<div class="form-group"><label for="f-ddb0-auth_username"><?= $this->te('Benutzername') ?></label><input id="f-ddb0-auth_username" type="text" name="auth_username" class="form-control" style="max-width:320px;"></div>
<div class="form-group"><label for="f-ddb0-auth_password"><?= $this->te('Passwort') ?></label><input id="f-ddb0-auth_password" type="password" name="auth_password" class="form-control" autocomplete="new-password" style="max-width:480px;"></div>
</div>
<div class="auth-fields" data-auth="login" style="display:none;">
<div id="loginFields">
<?php if (!empty($login_fields)): ?>
<?php foreach ($login_fields as $lf): $name = (string)($lf['name'] ?? ''); if ($name === '') continue; ?>
<div class="form-group">
<label><?= $this->e((string)($lf['label'] ?? $name)) ?><?php if (!empty($lf['optional'])): ?> <small class="text-muted"><?= $this->te('(optional)') ?></small><?php endif; ?></label>
<input type="<?= !empty($lf['secret']) ? 'password' : 'text' ?>" name="login_creds[<?= $this->e($name) ?>]" class="form-control" autocomplete="new-password" style="max-width:480px;">
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="small text-muted mb-0"><?= $this->te('Die Login-Felder werden von der KI-Analyse erkannt. Bitte zunächst „KI analysieren lassen" ausführen.') ?></p>
<?php endif; ?>
</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" id="btnSaveCreds">
<i class="fas fa-save"></i> <?= $this->te('Zugangsdaten speichern') ?>
</button>
<span id="credResult" class="ml-2 small"></span>
</form>
</div>
</div>
<?php endif; ?>
<!-- ── Zeitplan ────────────────────────────────────────────────────── -->
<?php if ($can_create): ?>
<div class="card card-outline card-secondary mb-3">
<div class="card-header"><h3 class="card-title"><?= $this->te('Zeitplan') ?></h3></div>
<div class="card-body">
<form method="POST" action="/admin/api-auto/<?= $cid ?>/schedule">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="form-group">
<label for="f-ddb0-schedule_mode"><?= $this->te('Ausführung') ?></label>
<select id="f-ddb0-schedule_mode" name="schedule_mode" class="form-control" style="max-width:320px;">
<option value="manual" <?= $c['schedule_mode'] === 'manual' ? 'selected' : '' ?>><?= $this->te('Einmalig / manuell') ?></option>
<option value="recurring" <?= $c['schedule_mode'] === 'recurring' ? 'selected' : '' ?>><?= $this->te('Regelmäßig (Scheduler)') ?></option>
</select>
</div>
<div class="form-group">
<label for="f-ddb0-frequency_minutes"><?= $this->te('Intervall (Minuten)') ?></label>
<input id="f-ddb0-frequency_minutes" type="number" name="frequency_minutes" class="form-control" style="max-width:200px;"
min="1" max="525600" value="<?= (int)$c['frequency_minutes'] ?>">
</div>
<div class="form-group form-check">
<input type="checkbox" name="enabled" value="1" class="form-check-input" id="schedEnabled" <?= $c['enabled'] ? 'checked' : '' ?>>
<label class="form-check-label" for="schedEnabled"><?= $this->te('Aktiv') ?></label>
</div>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Zeitplan speichern') ?></button>
<small class="form-text text-muted mt-2">
<?= $this->t('Regelmäßige Läufe erfordern einen erfolgreichen Dry-Run sowie den aktivierten Cron-Task „API-Auto Verbindungen" unter <a href="/admin/cronjobs">/admin/cronjobs</a>.') ?>
</small>
</form>
</div>
</div>
<?php endif; ?>
<!-- ── Bearbeiten ──────────────────────────────────────────────────── -->
<?php if ($can_create): ?>
<div class="card card-outline card-primary mb-3">
<div class="card-header"><h3 class="card-title"><?= $this->te('Verbindung bearbeiten') ?></h3></div>
<div class="card-body">
<form method="POST" action="/admin/api-auto/<?= $cid ?>" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="form-group">
<label for="f-ddb0-name"><?= $this->te('Name') ?> <span class="text-danger">*</span></label>
<input id="f-ddb0-name" type="text" name="name" class="form-control" required maxlength="200" value="<?= $this->e($c['name']) ?>">
</div>
<div class="form-group">
<label for="f-ddb0-description"><?= $this->te('Beschreibung') ?></label>
<input id="f-ddb0-description" type="text" name="description" class="form-control" value="<?= $this->e($c['description'] ?? '') ?>">
</div>
<div class="form-group">
<label for="f-ddb0-base_url"><?= $this->te('Basis-URL') ?> <span class="text-danger">*</span></label>
<input id="f-ddb0-base_url" type="url" name="base_url" class="form-control" required value="<?= $this->e($c['base_url']) ?>">
</div>
<div class="form-group">
<label for="f-ddb0-target_entity"><?= $this->te('Ziel-Modul') ?> <span class="text-danger">*</span></label>
<select id="f-ddb0-target_entity" name="target_entity" class="form-control" required>
<option value=""><?= $this->te('– bitte wählen –') ?></option>
<?php foreach ($targets as $t): ?>
<option value="<?= $this->e($t->key()) ?>" <?= $c['target_entity'] === $t->key() ? 'selected' : '' ?>><?= $this->e($t->label()) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="f-ddb0-direction"><?= $this->te('Richtung') ?></label>
<select id="f-ddb0-direction" name="direction" class="form-control">
<option value="import" <?= ($c['direction'] ?? 'import') === 'import' ? 'selected' : '' ?>><?= $this->te('Import (extern → OpenNIT)') ?></option>
<option value="export" <?= ($c['direction'] ?? 'import') === 'export' ? 'selected' : '' ?>><?= $this->te('Export (OpenNIT → extern)') ?></option>
</select>
</div>
<div class="form-group col-md-4">
<label for="f-ddb0-sync_mode"><?= $this->te('Sync-Modus') ?></label>
<select id="f-ddb0-sync_mode" name="sync_mode" class="form-control">
<option value="full" <?= ($c['sync_mode'] ?? 'full') === 'full' ? 'selected' : '' ?>><?= $this->te('Voll (alles)') ?></option>
<option value="incremental" <?= ($c['sync_mode'] ?? 'full') === 'incremental' ? 'selected' : '' ?>><?= $this->te('Inkrementell (nur Neues)') ?></option>
</select>
</div>
<div class="form-group col-md-4">
<label><?= $this->te('Max. Datensätze/Lauf') ?></label>
<input type="number" name="max_records" class="form-control" min="0" placeholder="<?= $this->te('leer = Standard') ?>"
value="<?= !empty($c['max_records']) ? (int)$c['max_records'] : '' ?>">
</div>
</div>
<?php
// Nicht jedes Ziel kann Datensätze deaktivieren: Kontakte, Firmen und
// SSL-Einträge führen keinen Status, über den sich das abbilden ließe.
$kannDeaktivieren = ($target ?? null) instanceof \OpenNIT\ApiAuto\SupportsDeactivation;
?>
<div class="form-group form-check">
<input type="checkbox" name="handle_deletions" value="1" class="form-check-input" id="handleDel"
<?= !empty($c['handle_deletions']) && $kannDeaktivieren ? 'checked' : '' ?>
<?= $kannDeaktivieren ? '' : 'disabled' ?>>
<label class="form-check-label" for="handleDel"><?= $this->te('Im Quellsystem gelöschte Datensätze deaktivieren (nur eigene Importe)') ?></label>
<?php if (!$kannDeaktivieren): ?>
<small class="text-muted" style="display:block;">
<?= $this->t('Für {ziel} nicht möglich – die Datensätze führen keinen Status, über den sich „nicht mehr vorhanden" abbilden ließe. Verschwundene Einträge bleiben stehen und sind von Hand zu prüfen.', ['ziel' => $this->e($target ? $target->label() : $this->t('dieses Ziel'))]) ?>
</small>
<?php endif; ?>
</div>
<div class="form-group form-check">
<input type="checkbox" name="notify_email" value="1" class="form-check-input" id="notifyEmail" <?= !empty($c['notify_email']) ? 'checked' : '' ?>>
<label class="form-check-label" for="notifyEmail"><?= $this->te('Bei Fehlern zusätzlich per E-Mail benachrichtigen') ?></label>
</div>
<?php if (($c['sync_mode'] ?? 'full') === 'incremental'): ?>
<p class="small text-muted">
<?= $this->te('Inkrementeller Cursor:') ?>
<code><?= $this->e($c['cursor_value'] ?? '—') ?></code>
</p>
<?php endif; ?>
<p class="small text-muted">
<i class="fas fa-key"></i> <?= $this->t('Zugangsdaten werden zentral im Abschnitt <strong>„Zugangsdaten"</strong> (oben) gepflegt.') ?>
</p>
<div class="form-group">
<label for="f-ddb0-spec_format"><?= $this->te('Format') ?></label>
<select id="f-ddb0-spec_format" name="spec_format" class="form-control">
<?php foreach (['openapi' => 'OpenAPI / Swagger', 'json' => 'JSON-Beispiel', 'text' => 'Freitext-Doku'] as $k => $lbl): ?>
<option value="<?= $k ?>" <?= $c['spec_format'] === $k ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="f-ddb0-spec_file"><?= $this->te('API-Beschreibung ersetzen (Datei)') ?></label>
<input id="f-ddb0-spec_file" type="file" name="spec_file" class="form-control-file" accept=".json,.yaml,.yml,.txt">
</div>
<div class="form-group">
<label for="f-ddb0-spec_raw"><?= $this->te('API-Beschreibung') ?></label>
<textarea id="f-ddb0-spec_raw" name="spec_raw" class="form-control" rows="8"><?= $this->e($c['spec_raw'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="f-ddb0-intent"><?= $this->te('Intent') ?></label>
<textarea id="f-ddb0-intent" name="intent" class="form-control" rows="3"><?= $this->e($c['intent'] ?? '') ?></textarea>
</div>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
</form>
</div>
</div>
<div class="card card-outline card-secondary mb-3">
<div class="card-body d-flex flex-wrap" style="gap:.5rem;">
<form method="POST" action="/admin/api-auto/<?= $cid ?>/duplicate">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="fas fa-copy"></i> <?= $this->te('Duplizieren') ?></button>
</form>
<?php if (($c['sync_mode'] ?? 'full') === 'incremental'): ?>
<form method="POST" action="/admin/api-auto/<?= $cid ?>/reset-cursor" onsubmit="return confirm('Sync-Cursor zurücksetzen? Der nächste Lauf holt wieder alle Daten.');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="fas fa-undo"></i> <?= $this->te('Sync-Cursor zurücksetzen') ?></button>
</form>
<?php endif; ?>
</div>
</div>
<div class="card card-outline card-danger mb-3">
<div class="card-body d-flex justify-content-between align-items-center">
<span><?= $this->te('Verbindung dauerhaft löschen.') ?></span>
<form method="POST" action="/admin/api-auto/<?= $cid ?>/delete" onsubmit="return confirm('Verbindung wirklich löschen?');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-danger btn-sm"><?= $this->te('Löschen') ?></button>
</form>
</div>
</div>
<?php endif; ?>
<!-- ── Historie ────────────────────────────────────────────────────── -->
<div class="card card-outline card-secondary mb-3">
<div class="card-header"><h3 class="card-title"><?= $this->te('Lauf-Historie') ?></h3></div>
<div class="card-body p-0">
<?php if (empty($runs)): ?>
<p class="text-muted p-3 mb-0"><?= $this->te('Noch keine Läufe.') ?></p>
<?php else: ?>
<table class="table table-sm mb-0">
<thead><tr><th scope="col"><?= $this->te('Start') ?></th><th scope="col"><?= $this->te('Status') ?></th><th scope="col"><?= $this->te('Neu') ?></th><th scope="col"><?= $this->te('Aktual.') ?></th><th scope="col"><?= $this->te('Überspr.') ?></th><th scope="col"><?= $this->te('Meldung') ?></th></tr></thead>
<tbody>
<?php foreach ($runs as $r): ?>
<tr>
<td class="small"><?= $this->e($r['started_at']) ?></td>
<td><span class="badge badge-<?= $r['status'] === 'success' ? 'success' : ($r['status'] === 'error' ? 'danger' : 'secondary') ?>"><?= $this->e($r['status']) ?></span></td>
<td><?= (int)$r['items_imported'] ?></td>
<td><?= (int)$r['items_updated'] ?></td>
<td><?= (int)$r['items_skipped'] ?></td>
<td class="small">
<?= $this->e($r['message'] ?? '') ?>
<?php if (!empty($r['errors_json'])): ?>
<?php $errs = json_decode((string)$r['errors_json'], true) ?: []; ?>
<details class="mt-1">
<summary class="text-danger" style="cursor:pointer;"><?= $this->te('{anzahl} Fehlerdetail(s)', ['anzahl' => count($errs)]) ?></summary>
<pre style="max-height:200px;overflow:auto;background:var(--red-50);padding:.5rem;border-radius:4px;font-size:11px;"><?= $this->e(json_encode($errs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?></pre>
</details>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<script>
(function () {
var CID = <?= $cid ?>;
var CSRF = <?= json_encode($this->csrf()) ?>;
var BASE = '/admin/api-auto/' + CID;
var msg = document.getElementById('apiAutoMsg');
function notify(text, type) {
msg.innerHTML = '<div class="alert alert-' + (type || 'info') + '">' + text + '</div>';
}
// Meldungen können vom Nutzer eingegebene Feldnamen enthalten; sie werden
// als Text eingesetzt, nicht als Markup.
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function post(url, body) {
var opts = { method: 'POST', headers: { 'X-CSRF-Token': CSRF, 'X-Requested-With': 'XMLHttpRequest' } };
if (body) { opts.body = body; }
return fetch(url, opts).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); });
}
// Auth-Typ-Auswahl der zentralen „Zugangsdaten"-Karte: blendet die passenden
// Eingabefelder ein/aus.
var authSel = document.getElementById('authType');
function toggleAuthFields() {
if (!authSel) { return; }
document.querySelectorAll('#credForm .auth-fields').forEach(function (el) {
el.style.display = (el.getAttribute('data-auth') === authSel.value) ? '' : 'none';
});
var card = document.getElementById('credCard');
if (card) { card.style.display = (authSel.value === 'none') ? 'none' : ''; }
}
if (authSel) { authSel.addEventListener('change', toggleAuthFields); toggleAuthFields(); }
var lastAuth = { type: 'none' };
// Baut die Eingabefelder für den Login-Flow (mehrere Zugangsfelder) in der
// zentralen Karte – mit denselben Feldnamen wie beim manuellen Speichern.
function renderLoginFields(login) {
var box = document.getElementById('loginFields');
if (!box) { return; }
box.innerHTML = '';
var fields = (login && login.fields) || [];
if (!fields.length) {
box.innerHTML = '<p class="small text-muted mb-0">Für diese Verbindung wurden keine Login-Felder erkannt.</p>';
return;
}
fields.forEach(function (f) {
var grp = document.createElement('div');
grp.className = 'form-group';
var lbl = document.createElement('label');
lbl.textContent = (f.label || f.name) + (f.optional ? ' (optional)' : '');
var inp = document.createElement('input');
inp.type = f.secret ? 'password' : 'text';
inp.className = 'form-control';
inp.name = 'login_creds[' + f.name + ']';
inp.autocomplete = 'new-password';
inp.style.maxWidth = '480px';
grp.appendChild(lbl); grp.appendChild(inp);
box.appendChild(grp);
});
}
var btnAnalyze = document.getElementById('btnAnalyze');
if (btnAnalyze) btnAnalyze.addEventListener('click', function () {
btnAnalyze.disabled = true; notify('KI analysiert die API (ggf. wird die Beschreibung automatisch aus der URL geladen)…', 'info');
post(BASE + '/analyze').then(function (res) {
btnAnalyze.disabled = false;
if (!res.ok) { notify(res.data.error || 'Fehler bei der KI-Analyse.', 'danger'); return; }
var d = res.data;
document.getElementById('suggestBox').style.display = '';
document.getElementById('suggestSummary').textContent = d.summary || '';
var meta = 'Erkanntes Ziel: ' + (d.target_label || '–');
if (d.base_url) { meta += ' · Basis-URL: ' + d.base_url; }
if (d.discovered) { meta += ' · Beschreibung automatisch geladen'; }
if (d.web_searched) { meta += ' · per Web-Suche recherchiert'; }
document.getElementById('suggestMeta').textContent = meta;
document.getElementById('suggestMapping').textContent = JSON.stringify(d.mapping, null, 2);
// Den erkannten Auth-Typ in die zentrale Zugangsdaten-Karte übernehmen.
lastAuth = d.auth || { type: 'none' };
var hint = document.getElementById('authHint');
if (authSel) {
authSel.value = (lastAuth.type || 'none');
if (lastAuth.type === 'login') { renderLoginFields(lastAuth.login); }
else if (lastAuth.type === 'api_key') {
var nameEl = document.querySelector('#credForm input[name="auth_key_name"]');
var inEl = document.querySelector('#credForm select[name="auth_key_in"]');
if (nameEl && lastAuth.name) { nameEl.value = lastAuth.name; }
if (inEl && lastAuth.in) { inEl.value = lastAuth.in; }
}
toggleAuthFields();
}
if (hint) { hint.style.display = d.needs_auth ? '' : 'none'; }
if (!d.needs_auth && document.getElementById('btnPreview')) {
notify('Analyse fertig – lade Vorschau…', 'success');
doPreview(true);
} else {
notify('Analyse fertig. Zugangsdaten unten eintragen, dann „Übernehmen & bereitstellen" und anschließend eine Vorschau (Dry-Run) ausführen.', 'success');
}
}).catch(function () { btnAnalyze.disabled = false; notify('Netzwerkfehler.', 'danger'); });
});
// Beide Aktionen (Übernehmen, Zugangsdaten speichern) lesen aus EINEM
// Formular – es gibt nur eine Eingabestelle für Zugangsdaten.
function credFormData() {
var form = document.getElementById('credForm');
var fd = form ? new FormData(form) : new FormData();
fd.set('_csrf', CSRF);
return fd;
}
var btnConfirm = document.getElementById('btnConfirm');
if (btnConfirm) btnConfirm.addEventListener('click', function () {
post(BASE + '/mapping', credFormData()).then(function (res) {
if (!res.ok) { notify(res.data.error || 'Fehler.', 'danger'); return; }
notify('Übernommen. Bitte jetzt eine Vorschau (Dry-Run) ausführen, dann ist die Verbindung produktiv einsatzbereit.', 'success');
setTimeout(function () { location.reload(); }, 1200);
});
});
// Mapping von Hand: prüft die Eingabe schon im Browser auf gültiges JSON und
// zeigt danach, was der Server verworfen oder zurechtgerückt hat.
var btnSaveMapping = document.getElementById('btnSaveMapping');
if (btnSaveMapping) btnSaveMapping.addEventListener('click', function () {
var feld = document.getElementById('mappingEditor');
var hinweise = document.getElementById('mappingHinweise');
hinweise.style.display = 'none';
try {
JSON.parse(feld.value);
} catch (e) {
hinweise.style.display = '';
hinweise.innerHTML = '<div class="alert alert-danger">Das ist kein gültiges JSON: ' + esc(e.message) + '</div>';
return;
}
var daten = new FormData();
daten.append('mapping_json', feld.value);
btnSaveMapping.disabled = true;
post(BASE + '/mapping-manual', daten).then(function (res) {
btnSaveMapping.disabled = false;
if (!res.ok) {
hinweise.style.display = '';
hinweise.innerHTML = '<div class="alert alert-danger">' + esc(res.data.error || 'Fehler.') + '</div>';
return;
}
// Gespeicherte Fassung zurückspielen: der Nutzer sieht, was wirklich gilt.
feld.value = JSON.stringify(res.data.mapping, null, 2);
var liste = (res.data.hinweise || []).map(function (h) { return '<li>' + esc(h) + '</li>'; }).join('');
hinweise.style.display = '';
hinweise.innerHTML = liste
? '<div class="alert alert-warning">Gespeichert, mit Anmerkungen:<ul style="margin:.4rem 0 0 1rem;">' + liste + '</ul></div>'
: '<div class="alert alert-success">Gespeichert. Bitte jetzt eine Vorschau ausführen.</div>';
});
});
var btnSaveCreds = document.getElementById('btnSaveCreds');
if (btnSaveCreds) btnSaveCreds.addEventListener('click', function () {
var out = document.getElementById('credResult');
btnSaveCreds.disabled = true;
if (out) { out.textContent = 'Speichere…'; out.className = 'ml-2 small text-muted'; }
post(BASE + '/credentials', credFormData()).then(function (res) {
btnSaveCreds.disabled = false;
if (!res.ok) {
if (out) { out.textContent = res.data.error || 'Fehler.'; out.className = 'ml-2 small text-danger'; }
return;
}
if (out) { out.textContent = 'Gespeichert. Bitte erneut eine Vorschau (Dry-Run) durchführen.'; out.className = 'ml-2 small text-success'; }
document.querySelectorAll('#credForm input[type="password"]').forEach(function (i) { i.value = ''; });
}).catch(function () {
btnSaveCreds.disabled = false;
if (out) { out.textContent = 'Netzwerkfehler.'; out.className = 'ml-2 small text-danger'; }
});
});
// Stellt dar, was der Lauf im Bestand bewirken würde. Beim Export gibt es
// nichts zu vergleichen – dann bleibt der Bereich leer.
function zeigeWirkung(w) {
var box = document.getElementById('previewWirkung');
if (!box) { return; }
if (!w) { box.innerHTML = ''; return; }
var teile = [
'<span class="badge badge-success">' + w.neu + ' neu</span>',
'<span class="badge badge-warning">' + w.geaendert + ' geändert</span>',
'<span class="badge badge-secondary">' + w.unveraendert + ' unverändert</span>'
].join(' ');
var html = '<div style="margin-bottom:.5rem;">' + teile + '</div>';
if (!w.vollstaendig) {
html += '<p class="small text-muted">Geprüft wurden die ersten ' + w.geprueft
+ ' Datensätze; der Rest würde entsprechend behandelt.</p>';
}
if ((w.aenderungen || []).length) {
html += '<div class="table-wrapper"><table class="data-table"><thead><tr>'
+ '<th>Datensatz</th><th>Feld</th><th>Vorher</th><th>Nachher</th></tr></thead><tbody>';
w.aenderungen.forEach(function (a) {
a.felder.forEach(function (f, i) {
html += '<tr><td>' + (i === 0 ? esc(a.schluessel) : '') + '</td>'
+ '<td><span class="badge">' + esc(f.feld) + '</span></td>'
+ '<td style="color:var(--gray-500);">' + (f.vorher === null ? '—' : esc(f.vorher)) + '</td>'
+ '<td>' + (f.nachher === null ? '—' : esc(f.nachher)) + '</td></tr>';
});
});
html += '</tbody></table></div>';
if (w.geaendert > w.aenderungen.length) {
html += '<p class="small text-muted">… und ' + (w.geaendert - w.aenderungen.length)
+ ' weitere geänderte Datensätze.</p>';
}
}
box.innerHTML = html;
}
function doPreview(auto) {
if (!auto) { notify('Vorschau wird geladen…', 'info'); }
var fd = new FormData();
fd.append('_csrf', CSRF);
return post(BASE + '/preview', fd).then(function (res) {
if (!res.ok) { notify(res.data.error || 'Fehler bei der Vorschau.', 'danger'); return; }
document.getElementById('previewBox').style.display = '';
document.getElementById('previewContent').textContent =
'Geholt: ' + res.data.fetched + ' Datensätze\n\n' + JSON.stringify(res.data.samples, null, 2);
zeigeWirkung(res.data.wirkung);
// Erfolgreicher Dry-Run auf gespeicherter Config gibt den Produktivlauf frei.
if (res.data.gate_passed) {
var badge = document.getElementById('previewBadge');
if (badge) { badge.className = 'badge badge-success'; badge.textContent = 'geprüft'; }
var run = document.getElementById('btnRun');
if (run && document.querySelector('#previewBadge')) { run.disabled = false; run.removeAttribute('title'); }
notify('Vorschau erfolgreich (' + res.data.fetched + ' Datensätze) – die Verbindung ist jetzt für den Produktivlauf freigegeben.', 'success');
} else {
notify('Vorschau geladen (' + res.data.fetched + ' Datensätze). Hinweis: Erst nach „Übernehmen & bereitstellen" zählt der Dry-Run als Freigabe.', 'success');
}
}).catch(function () { notify('Netzwerkfehler bei der Vorschau.', 'danger'); });
}
var btnPreview = document.getElementById('btnPreview');
if (btnPreview) btnPreview.addEventListener('click', function () { doPreview(false); });
var btnTest = document.getElementById('btnTest');
if (btnTest) btnTest.addEventListener('click', function () {
btnTest.disabled = true;
var tr = document.getElementById('testResult');
tr.innerHTML = '<span class="text-muted">Teste Verbindung…</span>';
var fd = new FormData(); fd.append('_csrf', CSRF);
post(BASE + '/test', fd).then(function (res) {
btnTest.disabled = false;
var ok = res.data && res.data.ok && res.data.message && res.data.message.indexOf('erfolgreich') !== -1;
tr.innerHTML = '<div class="alert alert-' + (ok ? 'success' : 'warning') + ' py-2 mb-0">' +
(res.data.message || 'Unbekanntes Ergebnis') + '</div>';
}).catch(function () { btnTest.disabled = false; tr.innerHTML = '<div class="alert alert-danger py-2 mb-0">Netzwerkfehler.</div>'; });
});
var btnRun = document.getElementById('btnRun');
if (btnRun) btnRun.addEventListener('click', function () {
if (!confirm('Verbindung jetzt ausführen und Daten übertragen?')) return;
btnRun.disabled = true; notify('Lauf läuft…', 'info');
post(BASE + '/run').then(function (res) {
btnRun.disabled = false;
if (!res.data.ok) { notify(res.data.message || res.data.error || 'Fehler beim Lauf.', 'danger'); return; }
notify('Lauf abgeschlossen: ' + res.data.message, 'success');
setTimeout(function () { location.reload(); }, 1200);
}).catch(function () { btnRun.disabled = false; notify('Netzwerkfehler.', 'danger'); });
});
})();
</script>