diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index 7839c21..4ddaa67 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -41,58 +41,69 @@ final class Navigation 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.hosts', 'server', 'hosts', null], - ['admin.datacenters', 'database', 'datacenters', 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], - ['admin.capacity', 'database', 'capacity', 'hosts.manage'], - // Hostnamen und ihre Zertifikate. Unter Betrieb und nicht unter - // System: was hier steht, entscheidet, ob eine Adresse - // ANTWORTET — das ist kein Einrichten, das ist Betrieb. Ohne - // diesen Eintrag war die Seite zwar vorhanden, aber über die - // Navigation nicht erreichbar, und eine Seite, die man nur mit - // der URL findet, gibt es für den Betreiber nicht. - ['admin.proxy-hosts', 'globe', 'proxy_hosts', 'site.manage'], + ]], + // Alles, wo Geld drinsteht. + ['label' => __('admin.nav_group.billing'), 'items' => [ ['admin.revenue', 'trending-up', 'revenue', null], - // Its own entry, not a section of Settings: what is set here - // appears on a legal document, and burying it beside the - // site-visibility switch invites somebody to change one in passing. - ['admin.finance', 'receipt', 'finance', 'site.manage'], ['admin.invoices', 'file-text', 'invoices', 'site.manage'], ['admin.payment-problems', 'alert-triangle', 'payment_problems', 'billing.manage'], - // What customers wrote to us, and what we sent them. Under - // Betrieb rather than System: both are read while answering - // somebody, not while configuring a machine. + // 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'], - // The answers, written once. Beside the two mail pages because - // that is where they are used, not under System — nothing about - // a template configures the installation. ['admin.templates', 'file-text', 'templates', 'customers.manage'], - ]], - ['label' => __('admin.nav_group.system'), 'items' => [ - ['admin.mail', 'mail', 'mail', 'mail.manage'], - // Looking at a mail before a customer does. Beside the mailboxes - // it is sent from, not under Betrieb: both are about how mail - // leaves this installation. + // 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'], - // The processing agreement. Under System with the other things - // that are true of the whole installation rather than of one - // customer. - ['admin.dpa', 'file-text', 'dpa', 'dpa.manage'], - // System, not Betrieb: the tunnel is how the console reaches - // the estate at all. Nothing is provisioned, monitored or - // updated through anything else, so it belongs beside the other - // things that have to work before any of the day's work can. - ['admin.vpn', 'shield', 'vpn', null], + ]], + // 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 @@ -106,6 +117,9 @@ final class Navigation // 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 diff --git a/lang/de/admin.php b/lang/de/admin.php index 71cf60a..578580c 100644 --- a/lang/de/admin.php +++ b/lang/de/admin.php @@ -6,7 +6,11 @@ return [ 'to_portal' => 'Zum Kundenportal', 'nav_group' => [ + 'customers' => 'Kunden', + 'fleet' => 'Flotte', 'operations' => 'Betrieb', + 'billing' => 'Abrechnung', + 'post' => 'Post', 'system' => 'System', ], diff --git a/lang/en/admin.php b/lang/en/admin.php index d1a703e..bf93609 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -6,7 +6,11 @@ return [ 'to_portal' => 'To customer portal', 'nav_group' => [ + 'customers' => 'Customers', + 'fleet' => 'Fleet', 'operations' => 'Operations', + 'billing' => 'Billing', + 'post' => 'Mail', 'system' => 'System', ], diff --git a/resources/views/components/shell/nav.blade.php b/resources/views/components/shell/nav.blade.php index 7f143b1..d4d164b 100644 --- a/resources/views/components/shell/nav.blade.php +++ b/resources/views/components/shell/nav.blade.php @@ -41,22 +41,43 @@ aside past the viewport — same mechanism
already relies on. --}}
@foreach ($groups as $group) + @php + // Sichtbarkeit EINMAL je Gruppe entscheiden, nicht erst je + // Eintrag. Vorher wurde die Überschrift bedingungslos gedruckt + // und stand deshalb auch über einer Gruppe, von der dieser + // Betreiber keinen einzigen Eintrag sehen darf — eine + // Überschrift mit nichts darunter liest sich wie ein + // Ladefehler, nicht wie eine Berechtigung. + // + // Bis zur Neuordnung der Konsolen-Navigation fiel das nicht + // auf, weil jede Gruppe mindestens einen ungeschützten Eintrag + // trug. „Post" tut das nicht. + // + // $capability ist ein einzelner Fähigkeitsname, ein Array von + // Namen im Sinne von „irgendeine davon" (App\Support\Navigation + // hält fest, welche Einträge das benutzen), oder null für + // „alle". Laravels eigenes can() liest ein Array als „alle + // davon" — die falsche Logik für einen Eintrag, den man mit + // der einen ODER der anderen Fähigkeit erreicht. Daher die + // ausgeschriebene Verzweigung statt eines bloßen ->can(). + $visible = collect($group['items'])->filter(function (array $item) { + $capability = $item[3]; + + if ($capability === null) { + return true; + } + + return is_array($capability) + ? collect($capability)->contains(fn ($one) => auth()->user()?->can($one)) + : (bool) auth()->user()?->can($capability); + }); + @endphp + @continue($visible->isEmpty()) @if (! empty($group['label']))

{{ $group['label'] }}

@endif