diff --git a/app/Livewire/Admin/ConfirmSwitchMode.php b/app/Livewire/Admin/ConfirmSwitchMode.php
new file mode 100644
index 0000000..6f01e6a
--- /dev/null
+++ b/app/Livewire/Admin/ConfirmSwitchMode.php
@@ -0,0 +1,46 @@
+authorize('secrets.manage');
+
+ // Ein Wert aus dem Aufrufort dieses Modals — dieselbe Prüfung, die
+ // switchMode() gleich noch einmal macht: ein unbekannter Modus 404t
+ // hier, statt eine Bestätigung für etwas zu zeigen, das nachher
+ // ohnehin still nichts tut.
+ abort_if(OperatingMode::tryFrom($mode) === null, 404);
+
+ $this->mode = $mode;
+ }
+
+ public function confirm(): void
+ {
+ $this->authorize('secrets.manage');
+ $this->dispatch('mode-switch-confirmed', mode: $this->mode);
+ $this->closeModal();
+ }
+
+ public function render()
+ {
+ return view('livewire.admin.confirm-switch-mode');
+ }
+}
diff --git a/app/Livewire/Admin/Integrations.php b/app/Livewire/Admin/Integrations.php
index bd0f8e7..a5ab5b3 100644
--- a/app/Livewire/Admin/Integrations.php
+++ b/app/Livewire/Admin/Integrations.php
@@ -7,6 +7,7 @@ use App\Services\Deployment\UpdateChannel;
use App\Services\Env\EnvFileEditor;
use App\Services\Env\InvalidEnvContentException;
use App\Services\Secrets\SecretVault;
+use App\Support\OperatingMode;
use App\Support\ProvisioningSettings;
use App\Support\Settings;
use Illuminate\Support\Facades\Artisan;
@@ -188,6 +189,34 @@ class Integrations extends Component
}
}
+ // ---- The operating mode — governs every vault entry below it. ----
+
+ /**
+ * Den Betriebsmodus umlegen.
+ *
+ * Hinter derselben Sperre wie der Tresor — `secrets.manage` plus bestätigtes
+ * Passwort — weil der Wechsel auf Live der Moment ist, ab dem echtes Geld
+ * fließt. Das ist keine Umschaltfläche zum Danebenklicken.
+ */
+ #[On('mode-switch-confirmed')]
+ public function switchMode(string $mode): void
+ {
+ $this->guardSecrets();
+
+ // Ein String aus dem Browser. tryFrom, nicht from.
+ $target = OperatingMode::tryFrom($mode);
+
+ if ($target === null) {
+ return;
+ }
+
+ OperatingMode::set($target);
+
+ $this->dispatch('notify', message: __('integrations.mode_switched', [
+ 'mode' => __('readiness.mode.'.$target->value),
+ ]));
+ }
+
// ---- Plain settings — App\Support\Settings, hosts.manage, no password. ----
public function saveInfra(): void
@@ -462,6 +491,7 @@ class Integrations extends Component
'canInfra' => $canInfra,
'unlocked' => $unlocked,
'usable' => $vault->isUsable(),
+ 'mode' => OperatingMode::current(),
'restart' => $restart,
'entries' => collect(SecretVault::REGISTRY)
->map(fn (array $meta, string $key) => [
diff --git a/lang/de/integrations.php b/lang/de/integrations.php
index 4551151..b10ccb7 100644
--- a/lang/de/integrations.php
+++ b/lang/de/integrations.php
@@ -4,6 +4,20 @@ return [
'title' => 'Integrationen',
'subtitle' => 'Angebundene Dienste, gruppiert nach Zweck — Zugangsdaten und Einstellungen nebeneinander, nicht auf getrennten Seiten nach Speicherort.',
+ // Der Umschalter — über allen Zugangsdaten, weil er regiert, welcher
+ // Platz je Eintrag gilt.
+ 'mode_title' => 'Betriebsmodus',
+ 'mode_body' => 'Entscheidet, welcher Platz je Zugangsdatum unten gilt. Ist der Testplatz leer, gilt im Testbetrieb der Live-Platz — außer beim Stripe-Schlüssel, der in keine Richtung zurückfällt.',
+ 'mode_switch_to_test' => 'Zu Testbetrieb wechseln',
+ 'mode_switch_to_live' => 'Zu Livebetrieb wechseln',
+ 'mode_switched' => 'Betriebsmodus umgelegt: :mode.',
+ 'mode_switch_title_test' => 'Wirklich zu Testbetrieb wechseln?',
+ 'mode_switch_body_test' => 'Ab sofort gilt für jedes Zugangsdatum der Testplatz — oder, wo dieser leer ist, der Live-Platz. Zahlungen laufen weiter über den aktiven Stripe-Schlüssel des jeweiligen Modus.',
+ 'mode_switch_confirm_test' => 'Umlegen',
+ 'mode_switch_title_live' => 'Wirklich zu Livebetrieb wechseln?',
+ 'mode_switch_body_live' => 'Ab sofort fließt echtes Geld: jede Bestellung wird über den Live-Stripe-Schlüssel abgerechnet, nicht mehr über den Testschlüssel. Das ist keine Umschaltfläche zum Danebenklicken.',
+ 'mode_switch_confirm_live' => 'Umlegen',
+
'payments_title' => 'Zahlungen (Stripe)',
'payments_body' => 'Der Secret Key, mit dem Zahlungen entgegengenommen und Webhooks geprüft werden.',
@@ -96,4 +110,4 @@ return [
'platform' => 'Plattform',
'env' => 'Umgebung (.env)',
],
-];
\ No newline at end of file
+];
diff --git a/lang/en/integrations.php b/lang/en/integrations.php
index 24ea100..0c13a19 100644
--- a/lang/en/integrations.php
+++ b/lang/en/integrations.php
@@ -4,6 +4,20 @@ return [
'title' => 'Integrations',
'subtitle' => 'Connected services, grouped by purpose — credentials and settings side by side, not on separate pages by storage mechanism.',
+ // The switch — above every credential, because it governs which slot of
+ // each entry below applies.
+ 'mode_title' => 'Operating mode',
+ 'mode_body' => 'Decides which slot of each credential below applies. In test mode, an empty test slot falls back to the live slot — except the Stripe key, which does not fall back either way.',
+ 'mode_switch_to_test' => 'Switch to test mode',
+ 'mode_switch_to_live' => 'Switch to live mode',
+ 'mode_switched' => 'Operating mode switched: :mode.',
+ 'mode_switch_title_test' => 'Really switch to test mode?',
+ 'mode_switch_body_test' => 'From now on, every credential uses its test slot — or, where that one is empty, the live slot. Payments continue to go through whichever Stripe key belongs to the active mode.',
+ 'mode_switch_confirm_test' => 'Switch',
+ 'mode_switch_title_live' => 'Really switch to live mode?',
+ 'mode_switch_body_live' => 'From now on, real money moves: every order is charged through the live Stripe key, not the test one. This is not a switch to click by accident.',
+ 'mode_switch_confirm_live' => 'Switch',
+
'payments_title' => 'Payments (Stripe)',
'payments_body' => 'The secret key used to accept payments and verify webhooks.',
@@ -96,4 +110,4 @@ return [
'platform' => 'Platform',
'env' => 'Environment (.env)',
],
-];
\ No newline at end of file
+];
diff --git a/resources/views/components/ui/icon.blade.php b/resources/views/components/ui/icon.blade.php
index e69ec28..24ea9cc 100644
--- a/resources/views/components/ui/icon.blade.php
+++ b/resources/views/components/ui/icon.blade.php
@@ -43,6 +43,7 @@
'globe' => '