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' => '', + 'chevron-left' => '', 'x' => '', 'dashboard' => '', 'server' => '', diff --git a/resources/views/layouts/auth.blade.php b/resources/views/layouts/auth.blade.php index c4005a8..a022a48 100644 --- a/resources/views/layouts/auth.blade.php +++ b/resources/views/layouts/auth.blade.php @@ -8,20 +8,73 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles - -
-
- - - - Clusev -
+ +
+ {{-- Brand panel — hidden below lg --}} + + + {{-- Form panel --}} +
+
+ {{-- wordmark for small screens (brand panel hidden) --}} +
+ + + + Clusev +
+ + {{ $slot }} +
+
@livewireScripts diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index 4506f09..6c37f64 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -1,33 +1,51 @@ -
-

Anmelden

-

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 -
+
+
+

Control Plane

+

Anmelden

+

Zugang zum Clusev-Panel.

+
+ +
- + - @error('email')

{{ $message }}

@enderror + class="{{ $field }}" placeholder="admin@clusev.local" /> + @error('email')

{{ $message }}

@enderror
- + - @error('password')

{{ $message }}

@enderror + class="{{ $field }}" placeholder="••••••••" /> + @error('password')

{{ $message }}

@enderror
-
diff --git a/resources/views/livewire/auth/password-change.blade.php b/resources/views/livewire/auth/password-change.blade.php index dc24ab5..994fcb1 100644 --- a/resources/views/livewire/auth/password-change.blade.php +++ b/resources/views/livewire/auth/password-change.blade.php @@ -1,31 +1,38 @@ -
-

Passwort ändern

-

Lege ein eigenes Passwort fest (min. 12 Zeichen, Groß-/Kleinschreibung + Zahl).

+@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 -
+
+
+

Sicherheit

+

Passwort ändern

+

Lege ein eigenes Passwort fest (min. 12 Zeichen, Groß-/Kleinschreibung + Zahl).

+
+ +
- - - @error('current')

{{ $message }}

@enderror + + + @error('current')

{{ $message }}

@enderror
- - - @error('password')

{{ $message }}

@enderror + + + @error('password')

{{ $message }}

@enderror
- - + +
- + + + Speichern & weiter + Speichere… +
diff --git a/resources/views/livewire/auth/two-factor-challenge.blade.php b/resources/views/livewire/auth/two-factor-challenge.blade.php index 8085a12..c007d9d 100644 --- a/resources/views/livewire/auth/two-factor-challenge.blade.php +++ b/resources/views/livewire/auth/two-factor-challenge.blade.php @@ -1,19 +1,34 @@ -
-

Zwei-Faktor-Bestätigung

-

Gib den 6-stelligen Code aus deiner Authenticator-App ein.

+@php + $field = 'h-14 w-full rounded-md border border-line bg-inset text-center font-mono text-2xl font-semibold tracking-[0.5em] text-ink caret-accent 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 -
+
+
+

Zwei-Faktor

+

Bestätigung

+

6-stelliger Code aus deiner Authenticator-App.

+
+ +
- - - @error('code')

{{ $message }}

@enderror + + + @error('code')

{{ $message }}

@enderror
- + + + Bestätigen + Prüfe… + + +
diff --git a/resources/views/livewire/auth/two-factor-setup.blade.php b/resources/views/livewire/auth/two-factor-setup.blade.php index 7cda438..f1c9064 100644 --- a/resources/views/livewire/auth/two-factor-setup.blade.php +++ b/resources/views/livewire/auth/two-factor-setup.blade.php @@ -1,30 +1,38 @@ -
-

Zwei-Faktor einrichten

-

Scanne den Code mit einer Authenticator-App (Aegis, Google Authenticator, …) und bestätige mit einem Code.

+@php + $field = 'h-14 w-full rounded-md border border-line bg-inset text-center font-mono text-2xl font-semibold tracking-[0.5em] text-ink caret-accent 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 -
-
+
+
+

Zwei-Faktor

+

Einrichten

+

Scanne den Code mit einer Authenticator-App (Aegis, Google Authenticator, …) und bestätige mit einem Code.

+
+ +
+
2FA-QR-Code
+
+

Geheimnis (manuell)

+

{{ $secret }}

+
-
-

Geheimnis (manuell)

-

{{ $secret }}

-
- -
+
- - - @error('code')

{{ $message }}

@enderror + + + @error('code')

{{ $message }}

@enderror
- + + + Aktivieren + Prüfe… +
diff --git a/resources/views/livewire/files/index.blade.php b/resources/views/livewire/files/index.blade.php index 242b06c..3808a99 100644 --- a/resources/views/livewire/files/index.blade.php +++ b/resources/views/livewire/files/index.blade.php @@ -37,7 +37,7 @@ @if (! $loop->first) / @endif - @else - + @endif
@@ -119,10 +119,10 @@ {{-- Row actions --}}
@unless ($isDir) - Download - Bearbeiten + Download + Bearbeiten @endunless - Löschen + Löschen
@endforeach diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index d123616..51cfe8d 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -205,7 +205,7 @@

{{ $key['fingerprint'] }}

- +
diff --git a/resources/views/livewire/services/index.blade.php b/resources/views/livewire/services/index.blade.php index c3e7cc3..8e64ea3 100644 --- a/resources/views/livewire/services/index.blade.php +++ b/resources/views/livewire/services/index.blade.php @@ -70,9 +70,9 @@ {{-- R5: wire to wire-elements/modal --}}
- Starten - Stopp - Neustart + Starten + Stopp + Neustart
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php deleted file mode 100644 index 26e294a..0000000 --- a/resources/views/welcome.blade.php +++ /dev/null @@ -1,223 +0,0 @@ - - - - - - - {{ config('app.name', 'Laravel') }} - - @fonts - - - @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) - @vite(['resources/css/app.css', 'resources/js/app.js']) - @else - - @endif - - -
- @if (Route::has('login')) - - @endif -
-
-
-
-

Let's get started

-

With so many options available to you,
we suggest you start with the following:

- - - -

- v{{ app()->version() }} - - View changelog - - - - -

-
-
- {{-- Laravel Logo --}} - - - - - - - - - - - {{-- 13 --}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - @if (Route::has('login')) - - @endif - - diff --git a/rules.md b/rules.md index 468100a..f8811af 100644 --- a/rules.md +++ b/rules.md @@ -22,6 +22,8 @@ - [ ] **R10** — Reused existing `@theme` tokens + Blade components (no ad-hoc colors/widgets). - [ ] **R11** — Records exposed in URLs are addressed by **UUID**, never the integer PK. - [ ] **R12** — every touched page loads in a **browser** at **HTTP 200** with **zero console errors** (the **loaded** state of `wire:init` pages, not just the skeleton). +- [ ] **R13** — route **paths + names are English** (`/settings`, not `/einstellungen`); German only in the visible label. +- [ ] **R14** — fonts are **self-hosted locally** (`resources/fonts/*.woff2` bundled by Vite + `@font-face` in `app.css`) — **no Google Fonts / CDN `` or `@import`**. - [ ] **Secrets** — `git status` shows **no** `.env*`, token, key, or home-dir dotfile staged. --- @@ -393,6 +395,37 @@ Route::get('/dateien', ...); // → use /files --- +## R14 — Fonts are self-hosted locally, never loaded from a CDN + +**Why.** A self-hosted control plane runs in private/air-gapped networks and must not leak +requests to third parties or break when the internet is unreachable. Every font ships **with the +app** — `.woff2` files in `public/fonts/`, declared via `@font-face` in `resources/css/app.css`. +**No** ``, no `@import url(...)` from a CDN, no `fonts.gstatic.com`. + +✅ **Correct** +```css +/* resources/css/app.css — local file in resources/fonts/, bundled + hashed by Vite + (relative url so it resolves on the Vite dev server AND in the prod build) */ +@font-face { + font-family: 'Space Grotesk'; + font-weight: 300 700; + font-display: swap; + src: url('../fonts/space-grotesk.woff2') format('woff2'); +} +``` +> Use a **relative** `url('../fonts/…')` (not `/fonts/…`): an absolute path resolves against the +> Vite dev-server origin in dev → 404. Relative lets Vite resolve + emit the asset for both modes. + +❌ **Forbidden** +```html + +``` +```css +@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch'); /* ❌ CDN import */ +``` + +--- + ## Appendix — Secret hygiene The project lives in `/home/nexxo/clusev` (its own git repo). The Gitea push token lives in