}> */ public static function portal(): array { return [ ['label' => null, 'items' => [ ['dashboard', 'gauge', 'overview', null], ['cloud', 'cloud', 'cloud', null], // The only portal entry with a condition. Start carries no // custom domain and cannot buy one, so the tab would lead // straight to the 403 its page raises. ['domain', 'globe', 'domain', 'use-custom-domain'], ['users', 'users', 'users', null], ['backups', 'database', 'backups', null], ]], ['label' => __('dashboard.nav_group.contract'), 'items' => [ ['billing', 'box', 'billing', null], ['invoices', 'receipt', 'invoices', null], ['settings', 'settings', 'settings', null], ['support', 'life-buoy', 'support', null], ]], ]; } /** @return array}> */ public static function console(): array { return [ // Allein und ohne Überschrift: die Seite, auf der man landet. ['label' => null, 'items' => [ ['admin.overview', 'gauge', 'overview', null], ]], // Wer bei uns ist und was er hat. ['label' => __('admin.nav_group.customers'), 'items' => [ ['admin.customers', 'users', 'customers', null], ['admin.instances', 'box', 'instances', null], ['admin.plans', 'tag', 'plans', 'plans.manage'], ]], // Die Maschinen und ihre Adressen — der Bestand, den man ansieht. ['label' => __('admin.nav_group.fleet'), 'items' => [ ['admin.hosts', 'server', 'hosts', null], ['admin.datacenters', 'database', 'datacenters', null], ['admin.capacity', 'database', 'capacity', 'hosts.manage'], // Hostnamen und ihre Zertifikate: was hier steht, entscheidet, // ob eine Adresse ANTWORTET. Kein Einrichten, sondern Bestand. ['admin.proxy-hosts', 'globe', 'proxy_hosts', 'site.manage'], // Stand vorher unter System, mit der Begründung, der Tunnel // müsse stehen, bevor irgendetwas geht. Das stimmt — gilt aber // genauso für Integrationen und Bereitschaft, die dort bleiben. // Der Unterschied: das hier ist eine LISTE VON GEGENSTELLEN, // die man beim Arbeiten ansieht, so wie die Hostliste. Dieselbe // Begründung, mit der Hostnamen schon aus System herauskamen. ['admin.vpn', 'shield', 'vpn', null], ]], // Was gerade läuft oder klemmt. Vorgänge, kein Bestand. // // Diese Gruppe trug bis hierher zwölf Einträge — Maschinen, Geld und // Post unter einem Wort, das auf alle drei zutrifft und deshalb // keinem hilft. Sie heißt weiter „Betrieb" und meint jetzt nur noch // das eine. ['label' => __('admin.nav_group.operations'), 'items' => [ ['admin.provisioning', 'activity', 'provisioning', null], ['admin.maintenance', 'alert-triangle', 'maintenance', null], ['admin.incidents', 'bell', 'incidents', null], ]], // Alles, wo Geld drinsteht. ['label' => __('admin.nav_group.billing'), 'items' => [ ['admin.revenue', 'trending-up', 'revenue', null], ['admin.invoices', 'file-text', 'invoices', 'site.manage'], ['admin.payment-problems', 'alert-triangle', 'payment_problems', 'billing.manage'], // Eigener Eintrag, kein Abschnitt der Einstellungen: was hier // gesetzt wird, steht auf einem Rechtsdokument, und neben dem // Sichtbarkeitsschalter ändert das jemand im Vorbeigehen. ['admin.finance', 'receipt', 'finance', 'site.manage'], ]], // Was geschrieben wird — gelesen beim Antworten, nicht beim // Einrichten. ['label' => __('admin.nav_group.post'), 'items' => [ ['admin.inbox', 'mail', 'inbox', 'customers.manage'], ['admin.mail-log', 'send', 'mail_log', 'customers.manage'], ['admin.templates', 'file-text', 'templates', 'customers.manage'], // Stand vorher unter System, neben den Postfächern. Sie zeigt // aber VORLAGEN, keine Postfächer — und gehört deshalb neben // die Vorlagen, die sie darstellt. ['admin.mail.preview', 'send', 'mail_preview', 'mail.manage'], ]], // Was einmal eingerichtet wird und danach gilt. ['label' => __('admin.nav_group.system'), 'items' => [ // Die Postfächer und der Server, über den Mail dieses Haus // verlässt. Einrichtung, anders als die vier Seiten unter Post. ['admin.mail', 'mail', 'mail', 'mail.manage'], // Merged from the former admin.secrets + admin.infrastructure // pages, which split the same subject by storage mechanism // instead of by what it configures. Reachable with EITHER // capability — see App\Livewire\Admin\Integrations — so the // entry stays visible to exactly who could reach at least one // of the two before: an array here means "any of these", // never "all of these" (contrast a single string elsewhere in // this file, which x-shell.nav checks with plain ->can()). ['admin.integrations', 'plug', 'integrations', ['hosts.manage', 'secrets.manage']], // Every check App\Support\Readiness knows about, gathered on // one page — same gate as Integrations just above, because // both halves it configures come back together here. ['admin.readiness', 'shield-check', 'readiness', ['hosts.manage', 'secrets.manage']], // Der AV-Vertrag: gilt für die ganze Installation, nicht für // einen Kunden. ['admin.dpa', 'file-text', 'dpa', 'dpa.manage'], // Two-factor enrolment used to sit here as an entry of its // own. It is a tab of Settings now — it is a setting, and it // was the one place an operator had to leave the settings page // to change something about their own access. The standalone // page stays reachable at its route, because that is where // RequireOperatorTwoFactor sends somebody who may not open // anything else yet, Settings included. ['admin.settings', 'settings', 'settings', null], ['admin.roles', 'users', 'roles', 'staff.manage'], ]], ]; } /** * The label of the entry the current request belongs to. * * Matched on the route rather than the path, because the console's path * changes between host-bound and fallback mode while its route names do * not. Null when the page is not in the navigation at all — a detail view, * a modal — and the caller then supplies its own title. */ public static function currentLabel(bool $console = false): ?string { $prefix = $console ? 'admin.nav.' : 'dashboard.nav.'; foreach ($console ? self::console() : self::portal() as $group) { foreach ($group['items'] as [$route, , $key]) { $matches = $console ? AdminArea::routeIs($route) : request()->routeIs($route); if ($matches) { return __($prefix.$key); } } } return null; } }