feat(versions)+fix: password-sudo service control, honest login, real versions page
- Service control now works with a sudo-PASSWORD credential (not only NOPASSWD): FleetService::sudoFn() defines a remote priv() that feeds the vaulted password to `sudo -S` over stdin (base64 over the encrypted channel, never on the argv) when not root, else falls back to sudo -n. Verified LIVE on 10.10.90.162: cron restart/stop/ start executed for real (ActiveEnterTimestamp advanced to now, is-active=inactive after stop, restarted clean), journal reads real entries via sudo. - Login brand panel: removed fabricated telemetry — fake "24 Hosts erreichbar", "cpu 31% mem 48% load 0.86", "clusev connect 10.10.90.0/24". Replaced with true capability lines (agentless SSH/phpseclib, TOFU host-key pinning, 2FA, audit log, AGPL). Only real claims now (per "nur eintragen was auch wirklich geht"). - New Version & Releases page (/versions, EN route per R13) with REAL data only: version from config/clusev.php, build SHA + branch read from .git at runtime, changelog parsed from a real CHANGELOG.md, real Gitea repo + AGPL license, honest update path (deploy commands) — NO fake updater / stars / forks / CVEs / "update available v2.5.0". Sidebar nav + tag/git-branch icons added. - Dummy-data sweep (12-auditor workflow + adversarial verify): 1 confirmed finding — removed the unused sine/cosine series() fake-sparkline generator in Dashboard.php; also fixed a stale "static seed" chart comment and a stale "mock listing" comment; derived the versions repo label from config (DRY). 5 false positives dismissed. Verified (R12): /login + /versions + all 7 routes 200 / 0 console errors; login fake-metrics gone + honest lines + fonts load; versions shows real version/build/ changelog/repo with zero fabricated stats. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
aa7a7b1db8
commit
875678d0cd
|
|
@ -0,0 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
Alle nennenswerten Änderungen an Clusev. Format angelehnt an
|
||||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||||
[SemVer](https://semver.org). Es gibt noch kein getaggtes Release —
|
||||
der Stand ist die laufende v1-Foundation-Entwicklung.
|
||||
|
||||
## 0.1.0-dev — Unreleased
|
||||
|
||||
### 2026-06-13
|
||||
- fix · Datei-Editor: Klick löste einen Alpine-Fehler aus (`@js()` kompiliert nicht in Komponenten-Attributen) — auf Index-Lookups umgestellt
|
||||
- fix · Dienst-Steuerung über das hinterlegte Sudo-Passwort (`sudo -S`) statt nur passwortloses sudo — start/stop/restart + Journal funktionieren live
|
||||
- feat · Selbst-gehostete Fonts (kein CDN), Split-Brand Login/2FA, diese Versions-Seite
|
||||
- chore · Vollständige Regel-Auditierung (R1–R14), neue Regel R14 (lokale Fonts)
|
||||
|
||||
### 2026-06-12
|
||||
- feat · Dashboard mit Live-Sparklines und Dual-Chart (poll-getrieben)
|
||||
- feat · Server-Index + Detailseite, Dienste (systemd), Datei-Manager (SFTP), Audit-Log
|
||||
- feat · SSH-Schicht über phpseclib (exec + SFTP), Host-Key-Pinning (TOFU), Multi-Server-Switcher
|
||||
- feat · Echte Metriken (CPU/RAM/Disk/Load) inkl. Absolutwerten; Datei-Up-/Download/Editor
|
||||
- feat · Authentifizierung + 2FA (TOTP) mit erzwungenem Onboarding, R5-Bestätigungsmodals
|
||||
- feat · Deploy: install.sh + Caddy (Auto-TLS) + Produktions-Härtung
|
||||
- fix · Adversarial Review der SSH-Schicht (Escaping, Parser, Host-Key)
|
||||
|
||||
### 2026-06-11
|
||||
- chore · Projekt-Bootstrap: Dockerisiertes Laravel 13 + Livewire 3 + Tailwind 4 + Reverb, Regelwerk (rules.md) + Projektkontext (CLAUDE.md)
|
||||
|
|
@ -30,18 +30,6 @@ class Dashboard extends Component
|
|||
$this->ready = true;
|
||||
}
|
||||
|
||||
/** Deterministic wavy series — fallback sparkline for metrics without history. */
|
||||
private function series(float $base, int $n = 40, float $amp = 9): array
|
||||
{
|
||||
$out = [];
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$v = $base + sin($i / 3.0) * $amp + sin($i / 6.5) * ($amp * 0.55) + cos($i / 2.0) * 2;
|
||||
$out[] = (int) max(2, min(98, round($v)));
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Pad a real history series to a minimum length for a stable sparkline. */
|
||||
private function pad(array $values, float|int $current, int $min = 16): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Versions;
|
||||
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Version & releases page. Everything shown here is real: the version comes from
|
||||
* config, the build hash is read from .git at runtime, and the changelog is the
|
||||
* project's own CHANGELOG.md. There is no in-app updater (Clusev ships as a Docker
|
||||
* image) — the page states the actual update path instead of faking one.
|
||||
*/
|
||||
#[Layout('layouts.app')]
|
||||
#[Title('Version — Clusev')]
|
||||
class Index extends Component
|
||||
{
|
||||
public ?string $lastChecked = null;
|
||||
|
||||
/** Honest "check": there is no release server yet — report the real status. */
|
||||
public function checkUpdates(): void
|
||||
{
|
||||
$this->lastChecked = now()->format('H:i');
|
||||
$this->dispatch('notify', message: 'Entwicklungsversion — kein Release-Kanal konfiguriert.');
|
||||
}
|
||||
|
||||
/** Resolve the current commit from .git without needing the git binary. */
|
||||
private function build(): array
|
||||
{
|
||||
$root = base_path('.git');
|
||||
$head = @file_get_contents($root.'/HEAD');
|
||||
|
||||
if ($head === false) {
|
||||
return ['sha' => null, 'branch' => config('clusev.channel')];
|
||||
}
|
||||
|
||||
$head = trim($head);
|
||||
|
||||
if (! str_starts_with($head, 'ref: ')) {
|
||||
return ['sha' => substr($head, 0, 7), 'branch' => config('clusev.channel')];
|
||||
}
|
||||
|
||||
$ref = substr($head, 5);
|
||||
$branch = preg_replace('#^refs/heads/#', '', $ref);
|
||||
$sha = @file_get_contents($root.'/'.$ref);
|
||||
|
||||
if ($sha === false) {
|
||||
// packed-refs fallback
|
||||
foreach (preg_split('/\R/', (string) @file_get_contents($root.'/packed-refs')) as $line) {
|
||||
if (str_ends_with(trim($line), ' '.$ref)) {
|
||||
$sha = explode(' ', trim($line))[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['sha' => $sha ? substr(trim($sha), 0, 7) : null, 'branch' => $branch];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CHANGELOG.md into dated groups for the timeline.
|
||||
*
|
||||
* @return array<int, array{date:string, items:array<int, array{type:string, text:string}>}>
|
||||
*/
|
||||
private function changelog(): array
|
||||
{
|
||||
$path = base_path('CHANGELOG.md');
|
||||
if (! is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
$date = null;
|
||||
|
||||
foreach (preg_split('/\R/', (string) file_get_contents($path)) as $line) {
|
||||
if (preg_match('/^###\s+(.+)$/', $line, $m)) {
|
||||
$date = trim($m[1]);
|
||||
$groups[$date] = [];
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($date !== null && preg_match('/^-\s*(\w+)\s*·\s*(.+)$/u', trim($line), $m)) {
|
||||
$groups[$date][] = ['type' => strtolower($m[1]), 'text' => trim($m[2])];
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($groups as $d => $items) {
|
||||
if ($items !== []) {
|
||||
$out[] = ['date' => $d, 'items' => $items];
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.versions.index', [
|
||||
'version' => config('clusev.version'),
|
||||
'channel' => config('clusev.channel'),
|
||||
'repository' => config('clusev.repository'),
|
||||
'license' => config('clusev.license'),
|
||||
'build' => $this->build(),
|
||||
'groups' => $this->changelog(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,28 @@ class FleetService
|
|||
return (new SshClient($this->vault, timeout: 12))->connect($server);
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote `priv()` shell function: runs its arguments directly when already
|
||||
* root, otherwise via sudo. For a password credential the stored password is
|
||||
* fed to `sudo -S` over stdin (base64-encoded, so it never lands in the remote
|
||||
* process argv / `ps`); key/other auth falls back to passwordless `sudo -n`.
|
||||
* Prefix this to a command string, then invoke `priv <cmd>`.
|
||||
*/
|
||||
private function sudoFn(Server $server): string
|
||||
{
|
||||
$cred = $server->credential;
|
||||
$password = ($cred && $cred->auth_type === 'password') ? (string) $cred->secret : '';
|
||||
|
||||
if ($password !== '') {
|
||||
$b64 = base64_encode($password."\n");
|
||||
$branch = 'echo '.$b64.' | base64 -d | sudo -S -p \'\' "$@"';
|
||||
} else {
|
||||
$branch = 'sudo -n "$@"';
|
||||
}
|
||||
|
||||
return 'priv() { if [ "$(id -u)" = 0 ]; then "$@"; else '.$branch.'; fi; }; ';
|
||||
}
|
||||
|
||||
/** Split a marker-delimited compound output into [section => body]. */
|
||||
private function sections(string $out): array
|
||||
{
|
||||
|
|
@ -223,7 +245,7 @@ class FleetService
|
|||
$cmd = 'export LC_ALL=C; '
|
||||
.'echo '.self::MARK.'units===; systemctl list-units --type=service --all --plain --no-legend --no-pager; '
|
||||
.'echo '.self::MARK.'files===; systemctl list-unit-files --type=service --no-legend --no-pager; '
|
||||
.'echo '.self::MARK.'journal===; if [ "$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo -n"; fi; $SUDO journalctl -n '.(int) $journalLines.' --no-pager -o short-iso 2>&1';
|
||||
.'echo '.self::MARK.'journal===; '.$this->sudoFn($server).'priv journalctl -n '.(int) $journalLines.' --no-pager -o short-iso 2>&1';
|
||||
|
||||
try {
|
||||
$s = $this->sections($ssh->exec($cmd));
|
||||
|
|
@ -466,8 +488,7 @@ class FleetService
|
|||
|
||||
try {
|
||||
[$out, $code] = $ssh->run(
|
||||
'export LC_ALL=C; if [ "$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo -n"; fi; '
|
||||
."\$SUDO systemctl {$op} {$unit} 2>&1"
|
||||
'export LC_ALL=C; '.$this->sudoFn($server)."priv systemctl {$op} {$unit} 2>&1"
|
||||
);
|
||||
|
||||
return ['ok' => $code === 0, 'output' => trim($out)];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// Pre-release development build — no tagged release yet. The live build hash
|
||||
// is resolved from .git at runtime (see App\Livewire\Versions\Index).
|
||||
'version' => '0.1.0-dev',
|
||||
|
||||
'channel' => 'dev',
|
||||
|
||||
// Real project home (self-hosted Gitea) + license. Open-core, AGPL.
|
||||
'repository' => 'https://git.bave.dev/boban/clusev',
|
||||
|
||||
'license' => 'AGPL-3.0',
|
||||
];
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
$paths = [
|
||||
'menu' => '<path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/>',
|
||||
'chevron-left' => '<path d="m15 18-6-6 6-6"/>',
|
||||
'tag' => '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>',
|
||||
'git-branch' => '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
||||
'x' => '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
'dashboard' => '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
|
||||
'server' => '<rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/>',
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
<p class="px-3 pb-1 pt-3 font-mono text-[10px] uppercase tracking-widest text-ink-4">Konto</p>
|
||||
<x-nav-item icon="settings" href="/settings" :active="request()->is('settings*')">Einstellungen</x-nav-item>
|
||||
<x-nav-item icon="tag" href="/versions" :active="request()->is('versions*')">Version</x-nav-item>
|
||||
</nav>
|
||||
|
||||
{{-- User --}}
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@
|
|||
<span class="h-2 w-2 rounded-full bg-warning/50"></span>
|
||||
<span class="h-2 w-2 rounded-full bg-online/50"></span>
|
||||
</span>
|
||||
<span class="font-mono text-[10px] text-ink-4">ssh · control-plane</span>
|
||||
<span class="font-mono text-[10px] text-ink-4">clusev · sicherheitsmodell</span>
|
||||
</div>
|
||||
<div class="space-y-1.5 p-4 font-mono text-xs leading-relaxed">
|
||||
<p class="text-ink-2"><span class="text-ink-4">$</span> clusev connect <span class="text-accent-text">10.10.90.0/24</span></p>
|
||||
<p class="text-online">24 Hosts erreichbar · Host-Keys gepinnt</p>
|
||||
<p class="text-ink-2"><span class="text-ink-4">$</span> fleet status <span class="text-accent-text">--live</span></p>
|
||||
<p class="text-ink-3">cpu 31% · mem 48% · load 0.86 · 0 Alarme</p>
|
||||
<p class="flex items-center text-ink-2"><span class="text-ink-4">$</span><span class="ml-2 inline-block h-3.5 w-[7px] animate-pulse bg-accent"></span></p>
|
||||
<div class="space-y-2.5 p-4 font-mono text-xs leading-relaxed">
|
||||
<p class="flex items-center gap-2.5 text-ink-2"><span class="w-8 shrink-0 text-accent-text">ssh</span> agentenlos · exec + SFTP über phpseclib</p>
|
||||
<p class="flex items-center gap-2.5 text-ink-2"><span class="w-8 shrink-0 text-accent-text">key</span> Host-Keys via TOFU gepinnt</p>
|
||||
<p class="flex items-center gap-2.5 text-ink-2"><span class="w-8 shrink-0 text-accent-text">2fa</span> erzwungen für jeden Login</p>
|
||||
<p class="flex items-center gap-2.5 text-ink-2"><span class="w-8 shrink-0 text-accent-text">log</span> jede Aktion im Audit-Log</p>
|
||||
<p class="flex items-center gap-2.5 text-ink-3"><span class="w-8 shrink-0 text-cyan">core</span> Open Core · AGPL-lizenziert</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@php
|
||||
$svcLabel = ['online' => 'aktiv', 'warning' => 'degradiert', 'offline' => 'gestoppt'];
|
||||
|
||||
// dual-series chart paths (CPU + MEM) — static seed; live wiring via Reverb is a follow-up
|
||||
// Build SVG polyline paths from the real CPU/MEM history series (cache-backed, polled).
|
||||
$chart = function (array $s) {
|
||||
$w = 300; $h = 100; $pad = 6; $n = count($s); $pts = [];
|
||||
foreach ($s as $i => $v) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
$dirs = collect($entries)->where('type', 'dir')->count();
|
||||
$files = collect($entries)->where('type', 'file')->count();
|
||||
|
||||
// Human-readable byte sizes for the mock listing (real values come from SFTP later).
|
||||
// Human-readable byte sizes for the live SFTP listing.
|
||||
$fmtSize = function (?int $bytes): string {
|
||||
if ($bytes === null) {
|
||||
return '—';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
@php
|
||||
$tone = fn (string $t): string => ['feat' => 'accent', 'fix' => 'cyan', 'security' => 'accent'][$t] ?? 'neutral';
|
||||
$label = fn (string $t): string => ['feat' => 'Feature', 'fix' => 'Fix', 'chore' => 'Wartung', 'security' => 'Sicherheit', 'perf' => 'Tempo', 'style' => 'Design'][$t] ?? ucfirst($t);
|
||||
$repoHost = parse_url($repository, PHP_URL_HOST);
|
||||
$repoPath = trim((string) parse_url($repository, PHP_URL_PATH), '/');
|
||||
@endphp
|
||||
|
||||
<div class="space-y-5">
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">Version</p>
|
||||
<h2 class="mt-0.5 font-display text-xl font-semibold text-ink sm:text-2xl">Version & Releases</h2>
|
||||
</div>
|
||||
<x-btn variant="secondary" wire:click="checkUpdates" wire:target="checkUpdates" wire:loading.attr="disabled">
|
||||
<svg wire:loading wire:target="checkUpdates" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
|
||||
<x-icon name="rotate" class="h-3.5 w-3.5" wire:loading.remove wire:target="checkUpdates" />
|
||||
Nach Updates suchen
|
||||
</x-btn>
|
||||
</div>
|
||||
|
||||
{{-- Honest status banner — no fake "update available" --}}
|
||||
<div class="flex flex-wrap items-center gap-4 rounded-xl border border-line bg-surface p-5 shadow-panel">
|
||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-xl border border-accent/25 bg-accent/10 text-accent-text"><x-icon name="tag" class="h-6 w-6" /></span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="flex flex-wrap items-center gap-2 font-display text-base font-semibold text-ink">
|
||||
Clusev <span class="font-mono text-accent-text">v{{ $version }}</span>
|
||||
<x-badge tone="neutral">Entwicklung</x-badge>
|
||||
</p>
|
||||
<p class="mt-0.5 font-mono text-[11px] leading-relaxed text-ink-3">Aktueller Entwicklungsstand — noch kein getaggtes Release, kein Release-Kanal konfiguriert. Aktualisierung läuft über den Deploy (Image ziehen + migrieren).</p>
|
||||
</div>
|
||||
@if ($lastChecked)
|
||||
<span class="shrink-0 font-mono text-[11px] text-ink-4">geprüft {{ $lastChecked }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_300px]">
|
||||
{{-- Changelog timeline --}}
|
||||
<x-panel title="Änderungsprotokoll" :subtitle="count($groups) . ' Tage'" :padded="false">
|
||||
<div class="px-4 py-4 sm:px-5">
|
||||
@forelse ($groups as $g)
|
||||
<div class="flex gap-4 pb-6 last:pb-0">
|
||||
<div class="flex flex-col items-center">
|
||||
<span @class(['mt-1 h-2.5 w-2.5 shrink-0 rounded-full', 'bg-accent' => $loop->first, 'bg-ink-4' => ! $loop->first])></span>
|
||||
@unless ($loop->last)<span class="mt-1 w-px flex-1 bg-line"></span>@endunless
|
||||
</div>
|
||||
<div class="-mt-0.5 min-w-0 flex-1">
|
||||
<p class="font-mono text-xs text-ink-3">{{ $g['date'] }}</p>
|
||||
<ul class="mt-2 space-y-2">
|
||||
@foreach ($g['items'] as $it)
|
||||
<li class="flex items-start gap-2.5">
|
||||
<x-badge :tone="$tone($it['type'])" class="mt-0.5 shrink-0">{{ $label($it['type']) }}</x-badge>
|
||||
<span class="text-sm leading-relaxed text-ink-2">{{ $it['text'] }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="font-mono text-[11px] text-ink-3">Kein Änderungsprotokoll gefunden.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-panel>
|
||||
|
||||
{{-- Aside --}}
|
||||
<div class="space-y-5">
|
||||
<x-panel title="Installiert" :padded="false">
|
||||
<div class="space-y-3 px-4 py-4 sm:px-5">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-display text-2xl font-bold text-ink">v{{ $version }}</span>
|
||||
<span class="inline-flex items-center gap-1.5 font-mono text-[11px] text-online"><x-status-dot status="online" />läuft</span>
|
||||
</div>
|
||||
<dl class="space-y-2 font-mono text-[11px]">
|
||||
<div class="flex items-center justify-between gap-3"><dt class="text-ink-4">Build</dt><dd class="text-ink-2">{{ $build['sha'] ?? '—' }}</dd></div>
|
||||
<div class="flex items-center justify-between gap-3"><dt class="text-ink-4">Branch</dt><dd class="truncate text-ink-2">{{ $build['branch'] }}</dd></div>
|
||||
<div class="flex items-center justify-between gap-3"><dt class="text-ink-4">Kanal</dt><dd class="text-ink-2">{{ $channel }}</dd></div>
|
||||
<div class="flex items-center justify-between gap-3"><dt class="text-ink-4">Lizenz</dt><dd class="text-ink-2">{{ $license }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</x-panel>
|
||||
|
||||
<x-panel title="Aktualisierung" :padded="false">
|
||||
<div class="space-y-3 px-4 py-4 sm:px-5">
|
||||
<p class="font-mono text-[11px] leading-relaxed text-ink-3">Updates kommen über den Deploy — neues Image ziehen und Migrationen anwenden:</p>
|
||||
<pre class="overflow-x-auto rounded-md border border-line bg-void px-3 py-2.5 font-mono text-[11px] leading-relaxed text-ink-2">docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
artisan migrate --force</pre>
|
||||
</div>
|
||||
</x-panel>
|
||||
|
||||
<x-panel title="Projekt" :padded="false">
|
||||
<div class="space-y-3 px-4 py-4 sm:px-5">
|
||||
<a href="{{ $repository }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 rounded-md border border-line bg-inset px-3 py-2.5 transition-colors hover:border-accent/30">
|
||||
<span class="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-raised text-ink-3"><x-icon name="git-branch" class="h-4 w-4" /></span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-mono text-xs text-ink">{{ $repoPath }}</span>
|
||||
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $repoHost }}</span>
|
||||
</span>
|
||||
<x-icon name="chevron-left" class="h-4 w-4 shrink-0 rotate-180 text-ink-4" />
|
||||
</a>
|
||||
<p class="flex items-center gap-2 font-mono text-[11px] text-ink-3"><x-icon name="shield" class="h-3.5 w-3.5 shrink-0 text-cyan" /> Open Core · {{ $license }}</p>
|
||||
</div>
|
||||
</x-panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -8,6 +8,7 @@ use App\Livewire\Files;
|
|||
use App\Livewire\Servers;
|
||||
use App\Livewire\Services;
|
||||
use App\Livewire\Settings;
|
||||
use App\Livewire\Versions;
|
||||
use Illuminate\Support\Facades\Auth as AuthFacade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -41,5 +42,6 @@ Route::middleware('auth')->group(function () {
|
|||
Route::get('/audit', Audit\Index::class)->name('audit.index');
|
||||
|
||||
Route::get('/settings', Settings\Index::class)->name('settings');
|
||||
Route::get('/versions', Versions\Index::class)->name('versions');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue