diff --git a/CLAUDE.md b/CLAUDE.md
index 8ed83e2..f21ff06 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,7 +49,7 @@ dashboard**, connecting **agentless over SSH**. The operator sees and steers the
| SSH | **phpseclib** (exec + SFTP) |
| Charts | JS lib (ApexCharts / Chart.js / uPlot) as an **Alpine island** in Blade |
| Icons | **Lucide**, inline SVG via a Blade `x-icon` component |
-| Fonts | **Chakra Petch** (display) · **Space Grotesk** (sans) · **JetBrains Mono** (numbers/paths) — self-host `.woff2`, no CDN in prod |
+| Fonts | **Chakra Petch** (display) · **Space Grotesk** (sans) · **JetBrains Mono** (numbers/paths) — **self-hosted only** (`public/fonts/*.woff2` + `@font-face` in `app.css`), **never a CDN/Google Fonts link or `@import`** (R14) |
> **Version note (2026-06-11):** handoff §3 specifies Laravel 12; **Laravel 13** is used (it is the current release; user decision). Livewire 3 / Tailwind 4 / wire-elements/modal / Reverb are unaffected; PHP pinned to **8.3**.
@@ -156,6 +156,9 @@ number/IP/path**. All tokens live in `resources/css/app.css` (R3). Token groups:
PK (`getRouteKeyName(): 'uuid'`). (R11)
- **Route paths + names are English**, always — `/settings` not `/einstellungen`, `/files` not
`/dateien`. German lives in the visible nav label, never in the `href`/route name. (R13)
+- **Fonts self-hosted only** — `.woff2` in `resources/fonts/` (Vite-bundled, relative
+ `url('../fonts/…')`) + `@font-face` in `app.css`; never a Google Fonts / CDN `` or
+ `@import`. (R14)
- **Responsive:** every screen at **375 / 768 / 1280+**; sidebar → drawer on small; KPI grid
4→2→1; tables scroll/stack; touch targets ≥ 44px. (R7)
- **Docs language:** these meta-docs are in English; **UI strings are German**.
diff --git a/app/Livewire/Files/Index.php b/app/Livewire/Files/Index.php
index 410a004..3f90389 100644
--- a/app/Livewire/Files/Index.php
+++ b/app/Livewire/Files/Index.php
@@ -53,14 +53,20 @@ class Index extends Component
$this->ready = true;
}
- public function open(string $name): void
+ public function open(int $index): void
{
+ $name = $this->entries[$index]['name'] ?? null;
+ if ($name === null) {
+ return;
+ }
+
$this->path = rtrim($this->path, '/').'/'.$name;
$this->load();
}
- public function go(string $path): void
+ public function go(int $index): void
{
+ $path = $this->crumbs[$index]['path'] ?? '/';
$this->path = $path !== '' ? $path : '/';
$this->load();
}
@@ -92,10 +98,11 @@ class Index extends Component
}
/** Stream a remote file to the browser as a download (SFTP get). */
- public function download(string $name, FleetService $fleet)
+ public function download(int $index, FleetService $fleet)
{
+ $name = $this->entries[$index]['name'] ?? null;
$active = $this->activeServer();
- if (! $active || ! $active->credential_exists) {
+ if ($name === null || ! $active || ! $active->credential_exists) {
return null;
}
@@ -111,10 +118,11 @@ class Index extends Component
}
/** Open the view/edit modal for a file. */
- public function edit(string $name): void
+ public function edit(int $index): void
{
+ $name = $this->entries[$index]['name'] ?? null;
$active = $this->activeServer();
- if (! $active) {
+ if ($name === null || ! $active) {
return;
}
@@ -156,8 +164,13 @@ class Index extends Component
* Delete a file (R5): opens the confirm modal, which writes the AuditEvent
* and re-dispatches `fileConfirmed`. SFTP unlink is wired in behind this later.
*/
- public function confirmDelete(string $name): void
+ public function confirmDelete(int $index): void
{
+ $name = $this->entries[$index]['name'] ?? null;
+ if ($name === null) {
+ return;
+ }
+
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
diff --git a/app/Livewire/Servers/Show.php b/app/Livewire/Servers/Show.php
index 6f9e0cc..5b7a24b 100644
--- a/app/Livewire/Servers/Show.php
+++ b/app/Livewire/Servers/Show.php
@@ -95,8 +95,16 @@ class Show extends Component
* Revoke an SSH key (R5): opens the confirm modal, which writes the
* AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later.
*/
- public function confirmKeyRemoval(string $fingerprint, string $comment): void
+ public function confirmKeyRemoval(int $index): void
{
+ $key = $this->sshKeys[$index] ?? null;
+ if ($key === null) {
+ return;
+ }
+
+ $fingerprint = $key['fingerprint'];
+ $comment = $key['comment'];
+
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
diff --git a/app/Livewire/Services/Index.php b/app/Livewire/Services/Index.php
index 96240b1..aa9849f 100644
--- a/app/Livewire/Services/Index.php
+++ b/app/Livewire/Services/Index.php
@@ -61,8 +61,13 @@ class Index extends Component
* modal writes the AuditEvent and re-dispatches `serviceConfirmed`, which
* applyService() handles. SSH exec (systemctl) is wired in behind this later.
*/
- public function confirm(string $op, string $name): void
+ public function confirm(string $op, int $index): void
{
+ $name = $this->filteredServices[$index]['name'] ?? null;
+ if ($name === null) {
+ return;
+ }
+
[$heading, $phrase, $label, $action, $icon] = match ($op) {
'start' => ['Dienst starten', 'wird gestartet', 'Starten', 'service.start', 'power'],
'stop' => ['Dienst stoppen', 'wird gestoppt', 'Stoppen', 'service.stop', 'power'],
diff --git a/resources/css/app.css b/resources/css/app.css
index f8c3c69..a89a383 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -6,6 +6,50 @@
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
+/* ── Self-hosted fonts (R14 — local only, never a CDN/@import from Google) ─ */
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('../fonts/chakra-petch-400.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('../fonts/chakra-petch-500.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-weight: 600;
+ font-display: swap;
+ src: url('../fonts/chakra-petch-600.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Chakra Petch';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('../fonts/chakra-petch-700.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'Space Grotesk';
+ font-style: normal;
+ font-weight: 300 700;
+ font-display: swap;
+ src: url('../fonts/space-grotesk.woff2') format('woff2');
+}
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-style: normal;
+ font-weight: 100 800;
+ font-display: swap;
+ src: url('../fonts/jetbrains-mono.woff2') format('woff2');
+}
+
/* ── Clusev design tokens — "Tactical Terminal" (handoff §6) ───────────── */
@theme {
/* surfaces */
diff --git a/resources/fonts/chakra-petch-400.woff2 b/resources/fonts/chakra-petch-400.woff2
new file mode 100644
index 0000000..f7a6602
Binary files /dev/null and b/resources/fonts/chakra-petch-400.woff2 differ
diff --git a/resources/fonts/chakra-petch-500.woff2 b/resources/fonts/chakra-petch-500.woff2
new file mode 100644
index 0000000..ff49854
Binary files /dev/null and b/resources/fonts/chakra-petch-500.woff2 differ
diff --git a/resources/fonts/chakra-petch-600.woff2 b/resources/fonts/chakra-petch-600.woff2
new file mode 100644
index 0000000..e4d90d5
Binary files /dev/null and b/resources/fonts/chakra-petch-600.woff2 differ
diff --git a/resources/fonts/chakra-petch-700.woff2 b/resources/fonts/chakra-petch-700.woff2
new file mode 100644
index 0000000..de88f23
Binary files /dev/null and b/resources/fonts/chakra-petch-700.woff2 differ
diff --git a/resources/fonts/jetbrains-mono.woff2 b/resources/fonts/jetbrains-mono.woff2
new file mode 100644
index 0000000..4d09cda
Binary files /dev/null and b/resources/fonts/jetbrains-mono.woff2 differ
diff --git a/resources/fonts/space-grotesk.woff2 b/resources/fonts/space-grotesk.woff2
new file mode 100644
index 0000000..0f3474e
Binary files /dev/null and b/resources/fonts/space-grotesk.woff2 differ
diff --git a/resources/views/components/btn.blade.php b/resources/views/components/btn.blade.php
index fbd92f3..af346a3 100644
--- a/resources/views/components/btn.blade.php
+++ b/resources/views/components/btn.blade.php
@@ -1,4 +1,4 @@
-@props(['variant' => 'secondary', 'icon' => false, 'href' => null])
+@props(['variant' => 'secondary', 'icon' => false, 'href' => null, 'size' => 'sm'])
@php
// One button style for the whole app — compact, consistent (R10).
$variants = [
@@ -9,10 +9,14 @@
'ghost' => 'text-ink-2 hover:bg-raised hover:text-ink',
'ghost-danger' => 'text-ink-3 hover:bg-offline/10 hover:text-offline',
];
- $shape = $icon ? 'h-8 w-8 justify-center' : 'h-8 px-3';
- $classes = 'inline-flex items-center gap-1.5 rounded-md text-xs font-medium transition-colors '
+ // sm = compact toolbar/row buttons; lg = full-height primary CTA (≥44px touch target, R7).
+ $sizes = [
+ 'sm' => ($icon ? 'h-8 w-8' : 'h-8 px-3').' text-xs',
+ 'lg' => ($icon ? 'h-11 w-11' : 'h-11 px-4').' text-sm',
+ ];
+ $classes = 'inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors '
.'disabled:opacity-50 disabled:pointer-events-none '
- .$shape.' '.($variants[$variant] ?? $variants['secondary']);
+ .($sizes[$size] ?? $sizes['sm']).' '.($variants[$variant] ?? $variants['secondary']);
@endphp
@if ($href)
merge(['class' => $classes]) }}>{{ $slot }}
diff --git a/resources/views/components/icon.blade.php b/resources/views/components/icon.blade.php
index c40012b..331fdaf 100644
--- a/resources/views/components/icon.blade.php
+++ b/resources/views/components/icon.blade.php
@@ -3,6 +3,7 @@
// Lucide SVG paths (24x24, stroke). Add icons here as needed.
$paths = [
'menu' => '
Zugang zum Clusev-Control-Panel.
+@php + $field = 'h-11 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none'; + $label = 'mb-1.5 block font-mono text-[11px] uppercase tracking-wider text-ink-3'; + $err = 'mt-1.5 flex items-center gap-1.5 font-mono text-[11px] text-offline'; +@endphp -With so many options available to you,
we suggest you start with the following:
- v{{ app()->version() }} - - View changelog - - -
-