{{ __('integrations.title') }}

{{ __('integrations.subtitle') }}

{{-- ── The tabs ────────────────────────────────────────────────────── This was one narrow column of six cards: an operator connecting DNS scrolled past Stripe to reach it, and the raw .env editor sat at the bottom of everything. Split by what a section is FOR — services we buy from outside, our own machines, the file underneath — which is the same axis this page was rebuilt around when it stopped being split by storage mechanism. The tab lives in the query string, so a reload or a bookmark lands where the operator was. Unlocking survives a switch: the password confirmation is a session fact, not a property of a tab. --}}
@foreach ($tabs as $name) @endforeach
@if ($canSecrets && ! $usable) {{ __('secrets.no_key') }} @endif {{-- The one lock for everything write-only on this page: every vault entry below, and the .env editor further down. Plain settings never sit behind this — they were never gated by a password before, and nothing about merging the pages makes them more dangerous. --}} @if ($canSecrets && ! $unlocked)

{{ __('secrets.locked_title') }}

{{ __('secrets.locked_body') }}

{{ __('secrets.unlock') }}
@elseif ($canSecrets)

{{ __('secrets.unlocked_note') }}

@endif @if ($tab === 'services') {{-- The switch — above every vault entry, because it governs them: which slot of each credential below is in force. Same lock as the vault itself (secrets.manage + confirmed password): the mode a stranger could type into a form field is only ever applied through switchMode()'s own guard, but the button that offers it in the first place stays behind the same unlock as everything else on this page. --}} @if ($canSecrets)

{{ __('integrations.mode_title') }}

{{ __('integrations.mode_body') }}

{{ __('readiness.mode.'.$mode->value) }} @if ($unlocked) {{ $mode->isTest() ? __('integrations.mode_switch_to_live') : __('integrations.mode_switch_to_test') }} @endif
@endif {{-- Spaltenfluss, nicht Raster. Vorher stand hier `grid lg:grid-cols-2 lg:items-start`. Ein Raster legt Karte 1 und 2 in EINE Reihe, und die Reihe ist so hoch wie die höhere: neben der Zahlungskarte (zwei Zugangsdaten, Hinweise, Knöpfe) stand der kurze Katalog-Kasten mit einem halben Bildschirm Leere darunter, und dasselbe noch einmal eine Reihe tiefer. Bei fünf Karten sehr unterschiedlicher Höhe ist das nicht zu ordnen, indem man sie umsortiert — welche wie hoch wird, hängt an Berechtigungen und daran, ob entsperrt ist. `columns` füllt stattdessen von oben nach unten und bricht um, wenn die Spalte voll ist. `break-inside-avoid` an jeder Karte ist die Zeile, die es benutzbar macht: ohne sie zerreißt eine Karte mitten im Formular. --}}
{{-- Zahlungen (Stripe) — vault only, no plain setting belongs here. --}} @if ($canSecrets)

{{ __('integrations.payments_title') }}

{{ __('integrations.payments_body') }}

@if ($unlocked)
{{-- Beide Stripe-Werte auf einer Karte, beide im Tresor, beide mit Test-/Live-Platz. Vorher stand hier ein blauer Kasten, der erklärte, warum dieser eine Wert in der Serverdatei bleiben MUSS — die Begründung hielt nicht (siehe App\Support\StripeWebhookSecret), und sie kostete den Betreiber einen Wert, den er nur per Dateieditor setzen konnte, unter einer Prüfung, die ihn als blockierend meldete. --}}
{{-- Der dritte Wert, den Stripe ausgibt. KEIN Tresoreintrag: er ist öffentlich, und ihn hinter „nie wieder vollständig angezeigt" zu setzen wäre Theater — man könnte nicht einmal nachsehen, welcher eingetragen ist. Also im Klartext, mit demselben Platz je Betriebsmodus wie die zwei darüber. --}}

{{ __('secrets.item.stripe_publishable_key') }}

STRIPE_KEY / STRIPE_KEY_TEST

@php $pkMode = \App\Support\OperatingMode::ofStripeKey($stripePublishableKey); @endphp @if ($pkMode !== null && $pkMode !== $mode)

{{ __('secrets.publishable_wrong_mode', ['mode' => __('readiness.mode.'.$pkMode->value)]) }}

@endif
{{ __('secrets.save') }}
{{ __('secrets.webhook_secret_note') }}

{{ __('secrets.publishable_note') }}

@endif
@endif {{-- Der Katalog-Abgleich. Eigene Karte statt eines Knopfes in der Zahlungskarte darüber: er benutzt zwar den Stripe-Schlüssel, ist aber nicht dessen Verwaltung — und er hängt an `plans.manage`, nicht am Tresor-Schloss. Bis hierher konnte er nur in einer Shell laufen, während die Bereitschaftsseite sein Fehlen als blockierend meldete. --}} @if ($canCatalogue)

{{ __('integrations.catalogue_title') }}

{{ __('integrations.catalogue_body') }}

{{-- Trockenlauf zuerst, und ohne Rückfrage: er ändert nichts. --}} {{ __('integrations.catalogue_dry_run') }} {{ __('integrations.catalogue_sync') }}
@if ($catalogueSync === null)

{{ __('integrations.catalogue_never_run') }}

@else

{{ $catalogueSync['ok'] ? __('integrations.catalogue_ok') : __('integrations.catalogue_failed') }} {{ $catalogueSync['dry_run'] ? __('integrations.catalogue_was_dry_run') : __('integrations.catalogue_was_live_run') }} — {{ $catalogueSync['at']->local()->isoFormat('D. MMM YYYY, HH:mm') }}, {{ $catalogueSync['by'] }}

{{-- Wortgleich, nicht zusammengefasst: zwei der Meldungen dieses Befehls sind ANWEISUNGEN — die Verweigerung nennt drei Spalten, die geleert werden müssen. Wer sie zu „fehlgeschlagen" kürzt, schickt den Betreiber zurück ins Terminal. Eigener Scrollbereich, weil ein erster Lauf eine Zeile je Produkt und Preis druckt. --}}
{{ $catalogueSync['output'] }}
@endif
@endif {{-- DNS (Hetzner) — token and the zone/Traefik settings that use it. --}} @if ($canSecrets || $canInfra)

{{ __('integrations.dns_title') }}

{{ __('integrations.dns_body') }}

@if ($canInfra)
@endif @if ($canSecrets) @if ($canInfra)
@endif @endif
@endif {{-- Das Support-Postfach — IMAP, nicht die mailcow-API: die API verwaltet Domains und Postfächer, gelesen wird ein Postfach über IMAP, egal wer den Server betreibt. --}} @if ($canSecrets || $canInfra)

{{ __('integrations.inbound_title') }}

{{ __('integrations.inbound_body') }}

@if ($canInfra)
@endif @if ($canSecrets) @if ($canInfra)
@endif @endif {{-- The one thing an operator wants after typing all of it: did any of it work. The button saves first, because testing what is on screen while the server still holds the old values would report on a mailbox nobody configured. --}} @if ($canInfra)
{{ __('integrations.inbound_test') }} {{-- Always said, including "never": a line that appears only after a success leaves an operator unable to tell a mailbox that has not been checked from one that is fine. R19: stored in UTC, read on the wall clock. --}} @if ($inboundStatus === null)

{{ __('integrations.inbound_never') }}

@else

$inboundStatus['ok'], 'text-danger' => ! $inboundStatus['ok']])> {{ __($inboundStatus['ok'] ? 'integrations.inbound_last_ok' : 'integrations.inbound_last_failed', [ 'when' => $inboundStatus['at']->local()->isoFormat('D. MMM YYYY, HH:mm'), 'ago' => $inboundStatus['at']->diffForHumans(), ]) }}

@endif
@endif
@endif {{-- Monitoring — token and where the Kuma bridge is reachable. --}} @if ($canSecrets || $canInfra)

{{ __('integrations.monitoring_title') }}

{{ __('integrations.monitoring_body') }}

@if ($canInfra) @endif @if ($canSecrets) @if ($canInfra)
@endif @endif
@endif
@endif @if ($tab === 'platform')
{{-- VPN/WireGuard — no vault entry: the private hub key stays in .env. --}} @if ($canInfra)

{{ __('integrations.vpn_title') }}

{{ __('integrations.vpn_body') }}

@endif {{-- SSH-Identität — public half a setting, private half the vault entry. --}} @if ($canSecrets || $canInfra)

{{ __('integrations.ssh_title') }}

{{ __('integrations.ssh_body') }}

@if ($canInfra)

{{ __('integrations.ssh_public_key_hint') }}

@error('sshPublicKey')

{{ $message }}

@enderror
@endif @if ($canSecrets) @if ($canInfra)
@endif @endif
@endif
@endif {{-- One button for both tabs' plain settings. Livewire keeps the values of a hidden field, so saving from either tab writes everything the operator has typed — which is what they mean by "speichern". --}} @if ($canInfra && in_array($tab, ['services', 'platform'], true))
{{ __('integrations.save_settings') }}
@endif {{-- Part B: the raw .env editor. Everything above has a field; everything that does not still needs to be reachable from the console, or an operator needing it needs a shell and this whole page misses the point. Shares the lock above: secrets.manage AND a confirmed password, because it is the one place on this page that can reach every credential the vault otherwise keeps write-only. --}} @if ($tab === 'env' && $canSecrets)

{{ __('integrations.env_title') }}

{{ __('integrations.env_subtitle') }}

@if (! $unlocked)

{{ __('integrations.env_locked') }}

@else

{{ __('integrations.env_danger_title') }}

{{ __('integrations.env_danger_body') }}

{{ __('integrations.env_vault_title') }}

{{ __('integrations.env_vault_body') }}

    @foreach ($entries as $entry)
  • {{ $entry['envKey'] }} {{ $entry['source'] === 'stored' ? __('integrations.env_vault_active') : __('integrations.env_vault_inactive') }}
  • @endforeach
@error('envContent')

{{ $message }}

@enderror
{{ __('integrations.env_save') }}

{{ __('integrations.env_backup_title') }}

{{ __('integrations.env_backup_body') }}

{{-- What saving now does automatically (config:clear here, the restart via UpdateChannel) — not homework anymore, see App\Livewire\Admin\Integrations::saveEnv(). The spinner below is this component's own wire:poll, not the full-screen deployment overlay: a restart of four containers is seconds, not minutes, and `app` — the container answering this very poll — is never among them, so this page never goes offline for it. --}}

{{ __('integrations.env_restart_title') }}

{{ __('integrations.env_restart_body') }}

@if ($envRestartWatching || $restart['restarting'])

{{ __('integrations.env_restarting') }}

@elseif (! $restart['agent_seen']) {{-- Honest, not silent: told plainly, with the fallback right here — same shape as the update card's own admin_settings.update_no_agent. --}} {{ __('admin_settings.update_no_agent') }}
docker compose restart queue queue-provisioning scheduler reverb
@endif
@endif
@endif