diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c7575d2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Git +.git +.gitignore + +# Node +node_modules + +# Laravel +vendor +storage/logs/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +bootstrap/cache/* + +# Test & Dev +tests +.env +.env.* +!.env.example + +# Docker selbst +docker-compose*.yml +Dockerfile +docker/ + +# IDE +.idea +.vscode +*.swp + +# OS +.DS_Store +Thumbs.db diff --git a/.env.example b/.env.example index c0660ea..fda7ec3 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ -APP_NAME=Laravel -APP_ENV=local +APP_NAME="Network-MGMT" +APP_ENV=production APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost +APP_DEBUG=false +APP_URL=http://localhost:8080 +APP_PORT=8080 APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -20,12 +21,13 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=network_mgmt +DB_USERNAME=network_mgmt +DB_PASSWORD=secret +DB_ROOT_PASSWORD=rootsecret SESSION_DRIVER=database SESSION_LIFETIME=120 @@ -63,3 +65,9 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# ─── Gitea Update-Checker ──────────────────────────────────────────────────── +# URL des Gitea-Servers (ohne trailing slash) +GITEA_URL=http://192.168.x.x:3000 +# Repository im Format owner/repo +GITEA_REPO=admin/Network-MGMT diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c443fb..a90e6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,40 @@ Dieses Projekt verwendet [Semantic Versioning](https://semver.org/lang/de/). --- +## [0.10.0] - 2026-07-02 + +### Added +- Docker-Setup: `Dockerfile` (PHP 8.5 FPM + nginx + Node.js 20 + nmap + supervisor), `docker-compose.yml` (App + MariaDB 11), automatischer Startup via `entrypoint.sh` (wartet auf DB, migriert, baut Assets, startet Supervisor) +- `docker/nginx.conf`, `docker/supervisord.conf`: nginx als Reverse-Proxy zu PHP-FPM, Laravel Scheduler als Supervisor-Job +- `.dockerignore` für minimale Image-Größe +- `.env.example` auf Docker-Betrieb angepasst (DB_HOST=db, MariaDB-Variablen, APP_PORT) +- Software-Update-Funktion: Admin → Einstellungen → 🔄 Software-Update +- `config/version.php`: zentrale Versionsverwaltung, konfigurierbare Gitea-URL +- Artisan-Command `app:check-update`: prüft Gitea-API auf neuere Release-Tags, speichert Ergebnis 6 Stunden im Cache +- Artisan-Command `app:install-update`: führt `git fetch`, `git checkout `, `composer install`, `npm run build`, `php artisan migrate`, Cache-Rebuild durch — mit automatischem Wartungsmodus +- `UpdateController` (Admin): Update-Seite mit Versionsvergleich, Release-Notes, Install-Button, manueller Tag-Eingabe +- Navigation: „Update"-Badge im Einstellungen-Dropdown wenn neue Version verfügbar (liest aus Cache, kein Live-HTTP-Request) +- Update-Check alle 6 Stunden via Laravel Scheduler (`app:check-update`) +- Wartungsseite `errors/maintenance.blade.php` mit Auto-Reload + +### Setup (einmalig) +``` +GITEA_URL=http://:3000 +GITEA_REPO=admin/Network-MGMT +``` +In `.env` eintragen, danach steht der Update-Check zur Verfügung. + +### Docker-Deployment +```bash +git clone http://:3000/admin/Network-MGMT.git +cd Network-MGMT +cp .env.example .env +# .env anpassen (Passwörter, APP_URL, GITEA_URL) +docker compose up -d --build +``` + +--- + ## [0.9.0] - 2026-07-02 ### Added @@ -18,10 +52,13 @@ Dieses Projekt verwendet [Semantic Versioning](https://semver.org/lang/de/). - Alle anderen Ereignisse auf dem Dashboard ebenfalls mit inline Notiz quittierbar - Geräte-Detailseite: IP-Verlauf zeigt Hinweis wenn ein Gerät mehrere verschiedene IPs hatte, alle bekannten IPs als Tags, aktuelle IP hervorgehoben - Globale Suche: `netbios_name` wird jetzt mitdurchsucht +- Globale Suche: Zeigt auch Treffer direkt aus `network_hosts` (Scan-Verlauf) wenn noch kein Geräteeintrag vorhanden +- Artisan-Command `network:backfill-devices`: Erstellt `network_devices`-Einträge für alle bestehenden `network_hosts` ohne `device_id` ### Setup (einmalig) ``` php artisan migrate +php artisan network:backfill-devices ``` --- @@ -152,7 +189,8 @@ php artisan migrate - Grundlegende PHP-Projektstruktur (public/, src/, config/) - composer.json, .gitignore, README.md -[Unreleased]: http://localhost:3000/admin/Network-MGMT/compare/v0.9.0...HEAD +[Unreleased]: http://localhost:3000/admin/Network-MGMT/compare/v0.10.0...HEAD +[0.10.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.9.0...v0.10.0 [0.9.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.8.0...v0.9.0 [0.8.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.7.0...v0.8.0 [0.7.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.6.0...v0.7.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9342420 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM php:8.5-fpm-bullseye + +# System-Abhängigkeiten +RUN apt-get update && apt-get install -y \ + git \ + curl \ + nginx \ + nmap \ + supervisor \ + libzip-dev \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + zip \ + unzip \ + default-mysql-client \ + && docker-php-ext-install pdo_mysql mbstring zip bcmath \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Node.js 20 +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Arbeitsverzeichnis +WORKDIR /var/www/html + +# Konfigurationsdateien +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Log-Verzeichnisse anlegen +RUN mkdir -p /var/log/supervisor + +# Port freigeben +EXPOSE 80 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/app/Console/Commands/AppCheckUpdateCommand.php b/app/Console/Commands/AppCheckUpdateCommand.php new file mode 100644 index 0000000..5353625 --- /dev/null +++ b/app/Console/Commands/AppCheckUpdateCommand.php @@ -0,0 +1,68 @@ +warn('GITEA_URL ist nicht konfiguriert.'); + Cache::put('app_update_available', null, now()->addHours(1)); + return Command::SUCCESS; + } + + try { + $response = Http::timeout(8) + ->get("{$giteaUrl}/api/v1/repos/{$repo}/releases/latest"); + + if (!$response->successful()) { + $this->warn("Gitea nicht erreichbar (HTTP {$response->status()})."); + Cache::put('app_update_check_error', "HTTP {$response->status()}", now()->addHours(1)); + return Command::SUCCESS; + } + + $latest = $response->json('tag_name', ''); + $available = !empty($latest) && version_compare( + ltrim($latest, 'v'), + ltrim($current, 'v'), + '>' + ); + + // Ergebnis 6 Stunden cachen + Cache::put('app_update_available', $available ? $latest : false, now()->addHours(6)); + Cache::put('app_update_checked_at', now()->toDateTimeString(), now()->addHours(6)); + Cache::forget('app_update_check_error'); + + if ($this->option('json')) { + $this->line(json_encode([ + 'current' => $current, + 'latest' => $latest, + 'available' => $available, + ])); + } else { + if ($available) { + $this->info("✓ Update verfügbar: {$current} → {$latest}"); + } else { + $this->info("✓ Kein Update — aktuelle Version: {$current}"); + } + } + } catch (\Exception $e) { + $this->error('Verbindungsfehler: ' . $e->getMessage()); + Cache::put('app_update_check_error', $e->getMessage(), now()->addHours(1)); + } + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/AppInstallUpdateCommand.php b/app/Console/Commands/AppInstallUpdateCommand.php new file mode 100644 index 0000000..3b2335e --- /dev/null +++ b/app/Console/Commands/AppInstallUpdateCommand.php @@ -0,0 +1,115 @@ +info(''); + $this->info('╔══════════════════════════════════════╗'); + $this->info('║ Network-MGMT Update ║'); + $this->info('╚══════════════════════════════════════╝'); + $this->info(''); + + // ── 1. Tags holen ────────────────────────────────────────────────────── + $this->info('▶ Git: Aktuelle Tags holen ...'); + $this->exec("cd {$projectPath} && git fetch --tags 2>&1"); + + // ── 2. Ziel-Tag bestimmen ────────────────────────────────────────────── + if ($this->option('tag')) { + $targetTag = $this->option('tag'); + } else { + $targetTag = trim(shell_exec( + "cd {$projectPath} && git describe --tags \$(git rev-list --tags --max-count=1) 2>/dev/null" + )); + } + + if (empty($targetTag)) { + $this->error('Kein Release-Tag im Repository gefunden.'); + return Command::FAILURE; + } + + $currentVersion = config('version.current'); + $this->info(" Aktuell: {$currentVersion}"); + $this->info(" Ziel: {$targetTag}"); + $this->info(''); + + if ($targetTag === $currentVersion) { + $this->warn('Bereits auf der neuesten Version. Abbruch.'); + return Command::SUCCESS; + } + + // ── 3. Wartungsmodus ─────────────────────────────────────────────────── + $this->info('▶ Wartungsmodus aktivieren ...'); + Artisan::call('down', ['--render' => 'errors.maintenance']); + + try { + // ── 4. Git Checkout ──────────────────────────────────────────────── + $this->info("▶ Git: Checkout {$targetTag} ..."); + $this->exec("cd {$projectPath} && git checkout {$targetTag} 2>&1"); + + // ── 5. Composer ──────────────────────────────────────────────────── + $this->info('▶ Composer: Abhängigkeiten aktualisieren ...'); + $this->exec("cd {$projectPath} && composer install --no-dev --optimize-autoloader --no-interaction 2>&1"); + + // ── 6. Assets ────────────────────────────────────────────────────── + if (file_exists("{$projectPath}/package.json")) { + $this->info('▶ Node: Assets neu bauen ...'); + $this->exec("cd {$projectPath} && npm ci --silent 2>&1"); + $this->exec("cd {$projectPath} && npm run build 2>&1"); + } + + // ── 7. Migrationen ───────────────────────────────────────────────── + $this->info('▶ Datenbank: Migrationen ausführen ...'); + Artisan::call('migrate', ['--force' => true, '--no-interaction' => true]); + $this->line(Artisan::output()); + + // ── 8. Cache leeren und neu aufbauen ─────────────────────────────── + $this->info('▶ Cache: Neu aufbauen ...'); + Artisan::call('config:cache'); + Artisan::call('route:cache'); + Artisan::call('view:cache'); + + // ── 9. Update-Cache zurücksetzen ─────────────────────────────────── + Cache::forget('app_update_available'); + Cache::forget('app_update_checked_at'); + + $this->info(''); + $this->info("✓ Update auf {$targetTag} erfolgreich abgeschlossen!"); + + } catch (\Throwable $e) { + $this->error('Update fehlgeschlagen: ' . $e->getMessage()); + Artisan::call('up'); + return Command::FAILURE; + + } finally { + // ── 10. Wartungsmodus deaktivieren ───────────────────────────────── + $this->info('▶ Wartungsmodus deaktivieren ...'); + Artisan::call('up'); + } + + return Command::SUCCESS; + } + + private function exec(string $cmd): void + { + $output = shell_exec($cmd); + if ($output) { + foreach (explode("\n", trim($output)) as $line) { + if (trim($line)) { + $this->line(" {$line}"); + } + } + } + } +} diff --git a/app/Console/Commands/NetworkBackfillDevicesCommand.php b/app/Console/Commands/NetworkBackfillDevicesCommand.php new file mode 100644 index 0000000..3641bc1 --- /dev/null +++ b/app/Console/Commands/NetworkBackfillDevicesCommand.php @@ -0,0 +1,132 @@ +option('dry-run'); + + // Alle Hosts ohne device_id, neueste zuerst (für korrekte last_seen_at) + $hosts = NetworkHost::query() + ->join('network_scans', 'network_scans.id', '=', 'network_hosts.scan_id') + ->whereNull('network_hosts.device_id') + ->select([ + 'network_hosts.*', + 'network_scans.created_at as scan_time', + ]) + ->orderByDesc('network_scans.created_at') + ->get(); + + $this->info("Gefunden: {$hosts->count()} Hosts ohne device_id"); + + if ($hosts->isEmpty()) { + $this->info('Nichts zu tun.'); + return Command::SUCCESS; + } + + $created = 0; + $linked = 0; + + foreach ($hosts as $host) { + $device = null; + + // 1. Per MAC suchen/erstellen + if (!empty($host->mac_address)) { + $device = NetworkDevice::where('mac_address', $host->mac_address)->first(); + + if (!$device) { + if (!$dryRun) { + $device = NetworkDevice::create([ + 'mac_address' => $host->mac_address, + 'current_ip' => $host->ip_address, + 'hostname' => $host->hostname, + 'mac_vendor' => $host->mac_vendor, + 'status' => $host->status, + 'first_seen_at' => $host->scan_time, + 'last_seen_at' => $host->scan_time, + ]); + $created++; + } + $this->line(" + Gerät (MAC) {$host->mac_address} / {$host->ip_address}"); + } + } + + // 2. Per Hostname suchen/erstellen (kein MAC) + elseif (!empty($host->hostname)) { + $device = NetworkDevice::whereNull('mac_address') + ->where('hostname', $host->hostname) + ->first(); + + if (!$device) { + if (!$dryRun) { + $device = NetworkDevice::create([ + 'mac_address' => null, + 'current_ip' => $host->ip_address, + 'hostname' => $host->hostname, + 'mac_vendor' => null, + 'status' => $host->status, + 'first_seen_at' => $host->scan_time, + 'last_seen_at' => $host->scan_time, + ]); + $created++; + } + $this->line(" + Gerät (Hostname) {$host->hostname} / {$host->ip_address}"); + } + } + + // 3. Per IP suchen/erstellen (weder MAC noch Hostname) + else { + $device = NetworkDevice::whereNull('mac_address') + ->where('current_ip', $host->ip_address) + ->first(); + + if (!$device) { + if (!$dryRun) { + $device = NetworkDevice::create([ + 'mac_address' => null, + 'current_ip' => $host->ip_address, + 'hostname' => null, + 'mac_vendor' => null, + 'status' => $host->status, + 'first_seen_at' => $host->scan_time, + 'last_seen_at' => $host->scan_time, + ]); + $created++; + } + $this->line(" + Gerät (IP) {$host->ip_address}"); + } + } + + // Host mit Gerät verknüpfen + if ($device && !$dryRun) { + $host->update(['device_id' => $device->id]); + + // Älteste first_seen_at beibehalten + if ($host->scan_time < $device->first_seen_at) { + $device->update(['first_seen_at' => $host->scan_time]); + } + + $linked++; + } + } + + if ($dryRun) { + $this->warn('Dry-Run – keine Änderungen geschrieben.'); + } else { + $this->info("✓ Fertig: {$created} Geräte neu erstellt, {$linked} Hosts verknüpft."); + } + + return Command::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/UpdateController.php b/app/Http/Controllers/Admin/UpdateController.php new file mode 100644 index 0000000..ca87502 --- /dev/null +++ b/app/Http/Controllers/Admin/UpdateController.php @@ -0,0 +1,76 @@ +get("{$giteaUrl}/api/v1/repos/{$giteaRepo}/releases/latest"); + + if ($response->successful()) { + $latestVersion = $response->json('tag_name', ''); + $releaseNotes = $response->json('body', ''); + $updateAvailable = !empty($latestVersion) && version_compare( + ltrim($latestVersion, 'v'), + ltrim($currentVersion, 'v'), + '>' + ); + + Cache::put('app_update_available', $updateAvailable ? $latestVersion : false, now()->addHours(6)); + Cache::put('app_update_checked_at', now()->toDateTimeString(), now()->addHours(6)); + $checkedAt = now()->toDateTimeString(); + } else { + $error = "Gitea nicht erreichbar (HTTP {$response->status()})."; + } + } catch (\Exception $e) { + $error = 'Verbindungsfehler: ' . $e->getMessage(); + } + } + + return view('admin.update', compact( + 'currentVersion', 'latestVersion', 'updateAvailable', + 'releaseNotes', 'error', 'checkedAt', 'giteaUrl', 'giteaRepo' + )); + } + + public function install(Request $request): RedirectResponse + { + $tag = $request->input('tag'); + + // Update-Command synchron ausführen und Ausgabe erfassen + Artisan::call('app:install-update', array_filter(['--tag' => $tag])); + $log = Artisan::output(); + + $success = !str_contains($log, 'fehlgeschlagen') && !str_contains($log, 'FAILURE'); + + return redirect()->route('admin.update.index')->with([ + 'update_result' => $success ? 'success' : 'error', + 'update_log' => $log, + ]); + } +} diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index a7b57aa..56639e1 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Services\SettingsService; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; @@ -29,5 +30,13 @@ class SettingsServiceProvider extends ServiceProvider 'theme_mode' => 'light', ]); } + + // Update-Badge: aus Cache lesen (kein HTTP-Request auf jeder Seite) + if (!$this->app->runningInConsole()) { + $updateAvailableVersion = Cache::get('app_update_available', null); + View::share('navUpdateAvailable', $updateAvailableVersion ?: false); + } else { + View::share('navUpdateAvailable', false); + } } } diff --git a/config/version.php b/config/version.php new file mode 100644 index 0000000..c7af6df --- /dev/null +++ b/config/version.php @@ -0,0 +1,21 @@ + 'v0.10.0', + + /* + * Gitea-Basis-URL (ohne trailing slash), z.B. http://192.168.1.10:3000 + * Wird aus der .env gelesen: GITEA_URL + */ + 'gitea_url' => env('GITEA_URL', ''), + + /* + * Gitea-Repository im Format "owner/repo" + * Wird aus der .env gelesen: GITEA_REPO + */ + 'gitea_repo' => env('GITEA_REPO', 'admin/Network-MGMT'), +]; diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..39857ef --- /dev/null +++ b/deploy.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════════════════════════ +# Network-MGMT — Deployment Script +# +# Auf dem LOKALEN Rechner ausführen: bash deploy.sh +# +# Voraussetzungen lokal: ssh, scp, tar +# Voraussetzungen Server: curl, systemd (Docker wird automatisch installiert) +# ═══════════════════════════════════════════════════════════════════════════════ + +set -euo pipefail + +# ── Farben ───────────────────────────────────────────────────────────────────── +R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m' +BOLD='\033[1m'; NC='\033[0m' + +# ── Banner ────────────────────────────────────────────────────────────────────── +echo "" +echo -e "${B}${BOLD} ╔══════════════════════════════════════════════╗${NC}" +echo -e "${B}${BOLD} ║ Network-MGMT — Deployment Script ║${NC}" +echo -e "${B}${BOLD} ╚══════════════════════════════════════════════╝${NC}" +echo "" + +# ── Eingaben ─────────────────────────────────────────────────────────────────── +echo -e "${BOLD}1/3 Server-Verbindung${NC}" +read -rp " Server-IP / Hostname : " SERVER_HOST +read -rp " SSH-Benutzer [root] : " SERVER_USER +SERVER_USER="${SERVER_USER:-root}" +read -rp " SSH-Port [22] : " SERVER_PORT +SERVER_PORT="${SERVER_PORT:-22}" +read -rp " Ziel-Pfad [/opt/network-mgmt] : " DEPLOY_PATH +DEPLOY_PATH="${DEPLOY_PATH:-/opt/network-mgmt}" + +echo "" +echo -e "${BOLD}2/3 App-Konfiguration${NC}" +read -rp " App-URL (z.B. http://${SERVER_HOST}:8080) : " APP_URL +APP_URL="${APP_URL:-http://${SERVER_HOST}:8080}" +read -rp " App-Port [8080] : " APP_PORT +APP_PORT="${APP_PORT:-8080}" + +echo "" +echo -e "${BOLD}3/3 Datenbank${NC}" +read -rsp " DB-Passwort (User) : " DB_PASSWORD; echo +read -rsp " DB-Root-Passwort : " DB_ROOT_PASSWORD; echo + +echo "" +echo -e "${BOLD}Optional: Gitea (Update-Funktion — jederzeit in .env nachträglich eintragbar)${NC}" +read -rp " Gitea-URL [leer lassen] : " GITEA_URL +GITEA_URL="${GITEA_URL:-}" + +# ── Zusammenfassung + Bestätigung ────────────────────────────────────────────── +echo "" +echo -e "${Y}─── Zusammenfassung ─────────────────────────────────────${NC}" +echo " Server : ${SERVER_USER}@${SERVER_HOST}:${SERVER_PORT}" +echo " Pfad : ${DEPLOY_PATH}" +echo " URL : ${APP_URL}" +echo " Port : ${APP_PORT}" +echo -e "${Y}─────────────────────────────────────────────────────────${NC}" +echo "" +read -rp "Deployment jetzt starten? [j/N] " CONFIRM +[[ "${CONFIRM,,}" == "j" ]] || { echo "Abgebrochen."; exit 0; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ARCHIVE="/tmp/network-mgmt-$(date +%Y%m%d_%H%M%S).tar.gz" +REMOTE_SCRIPT="/tmp/nm_setup_$$.sh" + +# ── App verpacken ────────────────────────────────────────────────────────────── +echo "" +echo -e "${Y}▶ App wird gepackt ...${NC}" +tar -czf "$ARCHIVE" \ + --exclude='.git' \ + --exclude='vendor' \ + --exclude='node_modules' \ + --exclude='.env' \ + --exclude='storage/logs' \ + --exclude='storage/framework/cache' \ + --exclude='storage/framework/sessions' \ + --exclude='storage/framework/views' \ + --exclude='bootstrap/cache' \ + --exclude='deploy.sh' \ + -C "$SCRIPT_DIR" . +echo -e "${G} ✓ Archiv: $(du -sh "$ARCHIVE" | cut -f1)${NC}" + +# ── Server-Setup-Skript erzeugen ─────────────────────────────────────────────── +# Variablen werden hier lokal eingebettet → kein SSH-Quoting-Problem +{ +printf '#!/bin/bash\nset -euo pipefail\n' +printf 'G='"'"'\033[0;32m'"'"'; Y='"'"'\033[1;33m'"'"'; BOLD='"'"'\033[1m'"'"'; NC='"'"'\033[0m'"'"'\n' +printf 'DEPLOY_PATH=%q\n' "$DEPLOY_PATH" +printf 'APP_URL=%q\n' "$APP_URL" +printf 'APP_PORT=%q\n' "$APP_PORT" +printf 'DB_PASSWORD=%q\n' "$DB_PASSWORD" +printf 'DB_ROOT_PASSWORD=%q\n' "$DB_ROOT_PASSWORD" +printf 'GITEA_URL=%q\n' "$GITEA_URL" +cat << 'REMOTE_BODY' + +echo "" +echo -e "${Y}▶ Docker prüfen / installieren ...${NC}" +if ! command -v docker &>/dev/null; then + curl -fsSL https://get.docker.com | sh + systemctl enable --now docker + echo -e "${G} ✓ Docker installiert: $(docker --version | awk '{print $3}' | tr -d ',')${NC}" +else + echo -e "${G} ✓ Docker: $(docker --version | awk '{print $3}' | tr -d ',')${NC}" +fi + +# Docker Compose Plugin +if ! docker compose version &>/dev/null 2>&1; then + echo -e "${Y} Docker Compose Plugin wird installiert ...${NC}" + mkdir -p "${HOME}/.docker/cli-plugins" + ARCH="$(uname -m)" + curl -fsSL \ + "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-${ARCH}" \ + -o "${HOME}/.docker/cli-plugins/docker-compose" + chmod +x "${HOME}/.docker/cli-plugins/docker-compose" +fi +echo -e "${G} ✓ Docker Compose: $(docker compose version --short 2>/dev/null || echo 'ok')${NC}" + +# App-Verzeichnis vorbereiten +echo "" +echo -e "${Y}▶ App entpacken nach ${DEPLOY_PATH} ...${NC}" +mkdir -p "$DEPLOY_PATH" + +# Bestehende .env sichern +ENV_BACKUP="" +if [ -f "${DEPLOY_PATH}/.env" ]; then + ENV_BACKUP="$(cat "${DEPLOY_PATH}/.env")" + echo -e "${G} ✓ Bestehende .env gesichert${NC}" +fi + +# Entpacken +tar -xzf /tmp/network-mgmt.tar.gz -C "$DEPLOY_PATH" +rm -f /tmp/network-mgmt.tar.gz +cd "$DEPLOY_PATH" + +# Storage-Verzeichnisse sicherstellen +mkdir -p storage/logs \ + storage/framework/cache \ + storage/framework/sessions \ + storage/framework/views \ + bootstrap/cache +chmod -R 775 storage bootstrap/cache + +# .env konfigurieren +if [ -n "$ENV_BACKUP" ]; then + echo "$ENV_BACKUP" > .env + echo -e "${G} ✓ Bestehende .env wiederhergestellt${NC}" +else + cp .env.example .env + sed -i "s|APP_URL=.*|APP_URL=${APP_URL}|" .env + sed -i "s|APP_PORT=.*|APP_PORT=${APP_PORT}|" .env + sed -i "s|DB_PASSWORD=secret|DB_PASSWORD=${DB_PASSWORD}|" .env + sed -i "s|DB_ROOT_PASSWORD=rootsecret|DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}|" .env + if [ -n "${GITEA_URL}" ]; then + sed -i "s|GITEA_URL=.*|GITEA_URL=${GITEA_URL}|" .env + fi + echo -e "${G} ✓ .env konfiguriert${NC}" +fi + +# Container bauen und starten +echo "" +echo -e "${Y}▶ Docker-Image bauen und starten (beim ersten Mal 3–5 Minuten) ...${NC}" +docker compose up -d --build + +# Warten bis App bereit +echo -e "${Y} Warte auf Container ...${NC}" +sleep 5 +for i in $(seq 1 20); do + if docker compose ps | grep -qE "healthy|Up"; then + break + fi + sleep 5 +done + +# Abschluss +echo "" +docker compose ps +echo "" +echo -e "${G}${BOLD}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${G}${BOLD}║ ✓ Network-MGMT erfolgreich installiert! ║${NC}" +echo -e "${G}${BOLD}║ ║${NC}" +printf "${G}${BOLD}║ URL: %-44s║${NC}\n" "${APP_URL}" +printf "${G}${BOLD}║ Pfad: %-44s║${NC}\n" "${DEPLOY_PATH}" +echo -e "${G}${BOLD}║ ║${NC}" +echo -e "${G}${BOLD}║ Logs: docker compose logs -f (im App-Pfad) ║${NC}" +echo -e "${G}${BOLD}║ Stop: docker compose down ║${NC}" +echo -e "${G}${BOLD}╚══════════════════════════════════════════════════════╝${NC}" +echo "" +REMOTE_BODY +} > "$REMOTE_SCRIPT" + +chmod +x "$REMOTE_SCRIPT" + +# ── Dateien auf Server übertragen ────────────────────────────────────────────── +echo "" +echo -e "${Y}▶ Übertrage auf ${SERVER_USER}@${SERVER_HOST} ...${NC}" +ssh -p "$SERVER_PORT" "${SERVER_USER}@${SERVER_HOST}" "mkdir -p '${DEPLOY_PATH}'" +scp -q -P "$SERVER_PORT" "$ARCHIVE" "${SERVER_USER}@${SERVER_HOST}:/tmp/network-mgmt.tar.gz" +scp -q -P "$SERVER_PORT" "$REMOTE_SCRIPT" "${SERVER_USER}@${SERVER_HOST}:/tmp/nm_setup.sh" +echo -e "${G} ✓ Übertragen${NC}" + +# Lokal aufräumen +rm -f "$ARCHIVE" "$REMOTE_SCRIPT" + +# ── Remote-Setup ausführen ───────────────────────────────────────────────────── +echo "" +echo -e "${Y}▶ Server-Setup läuft ...${NC}" +echo -e "${Y}─────────────────────────────────────────────${NC}" +ssh -p "$SERVER_PORT" "${SERVER_USER}@${SERVER_HOST}" "bash /tmp/nm_setup.sh; rm -f /tmp/nm_setup.sh" +echo -e "${Y}─────────────────────────────────────────────${NC}" + +echo "" +echo -e "${G}${BOLD}✓ Deployment abgeschlossen! → ${APP_URL}${NC}" +echo "" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c43ecf8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + + # ─── Laravel-App (PHP-FPM + nginx) ────────────────────────────────────────── + app: + build: + context: . + dockerfile: Dockerfile + container_name: network-mgmt-app + restart: unless-stopped + volumes: + - .:/var/www/html # App-Code (für git-pull-Updates) + - /var/www/html/node_modules # node_modules im Container halten + ports: + - "${APP_PORT:-8080}:80" + env_file: + - .env + depends_on: + db: + condition: service_healthy + networks: + - network-mgmt + + # ─── MariaDB ──────────────────────────────────────────────────────────────── + db: + image: mariadb:11 + container_name: network-mgmt-db + restart: unless-stopped + environment: + MARIADB_DATABASE: "${DB_DATABASE:-network_mgmt}" + MARIADB_USER: "${DB_USERNAME:-network_mgmt}" + MARIADB_PASSWORD: "${DB_PASSWORD:-secret}" + MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-rootsecret}" + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 15 + networks: + - network-mgmt + +networks: + network-mgmt: + driver: bridge + +volumes: + db_data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..5d78463 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -e + +echo "" +echo "╔══════════════════════════════════════╗" +echo "║ Network-MGMT Startup ║" +echo "╚══════════════════════════════════════╝" +echo "" + +cd /var/www/html + +# ─── .env prüfen ─────────────────────────────────────────────────────────────── +if [ ! -f ".env" ]; then + echo "⚠ .env fehlt — kopiere .env.example ..." + cp .env.example .env +fi + +# ─── Auf Datenbank warten ────────────────────────────────────────────────────── +echo "▶ Warte auf MariaDB ..." +until php -r " + \$dsn = 'mysql:host=' . getenv('DB_HOST') . ';port=' . (getenv('DB_PORT') ?: 3306) . ';dbname=' . getenv('DB_DATABASE'); + try { + new PDO(\$dsn, getenv('DB_USERNAME'), getenv('DB_PASSWORD')); + exit(0); + } catch (Exception \$e) { + exit(1); + } +" 2>/dev/null; do + echo " ... noch nicht bereit, warte 3s" + sleep 3 +done +echo " ✓ Datenbank erreichbar" + +# ─── Composer ────────────────────────────────────────────────────────────────── +if [ ! -d "vendor" ] || [ ! -f "vendor/autoload.php" ]; then + echo "▶ Composer: Abhängigkeiten installieren ..." + composer install --no-dev --optimize-autoloader --no-interaction --quiet + echo " ✓ fertig" +fi + +# ─── Assets bauen ────────────────────────────────────────────────────────────── +if [ ! -d "public/build" ] && [ -f "package.json" ]; then + echo "▶ Node: Assets bauen ..." + npm ci --silent 2>/dev/null + npm run build 2>/dev/null + echo " ✓ fertig" +fi + +# ─── App-Key ─────────────────────────────────────────────────────────────────── +APP_KEY_VAL=$(grep "^APP_KEY=" .env | cut -d= -f2) +if [ -z "$APP_KEY_VAL" ] || [ "$APP_KEY_VAL" = "" ]; then + echo "▶ Generiere APP_KEY ..." + php artisan key:generate --no-interaction --force +fi + +# ─── Storage-Link ────────────────────────────────────────────────────────────── +php artisan storage:link --no-interaction 2>/dev/null || true + +# ─── Berechtigungen ──────────────────────────────────────────────────────────── +chmod -R 775 storage bootstrap/cache +chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || true + +# ─── Migrationen ─────────────────────────────────────────────────────────────── +echo "▶ Datenbank: Migrationen ausführen ..." +php artisan migrate --force --no-interaction +echo " ✓ fertig" + +# ─── Cache ───────────────────────────────────────────────────────────────────── +echo "▶ Cache aufbauen ..." +php artisan config:cache --no-interaction 2>/dev/null +php artisan route:cache --no-interaction 2>/dev/null +php artisan view:cache --no-interaction 2>/dev/null +echo " ✓ fertig" + +echo "" +echo "✓ Network-MGMT läuft auf Port 80" +echo "" + +# ─── Supervisor starten (nginx + php-fpm + scheduler) ───────────────────────── +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..47b8f5c --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,43 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + # Datei-Upload bis 64 MB + client_max_body_size 64M; + + server { + listen 80; + server_name _; + root /var/www/html/public; + index index.php; + + # Laravel-Routen + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 120; + } + + # Dotfiles sperren + location ~ /\.(?!well-known) { + deny all; + } + + # Logs + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + } +} diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..39bb3a4 --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,38 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +loglevel=info + +; ─── PHP-FPM ─────────────────────────────────────────────────────────────────── +[program:php-fpm] +command=php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf +autostart=true +autorestart=true +priority=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; ─── nginx ───────────────────────────────────────────────────────────────────── +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +; ─── Laravel Scheduler (jede Minute) ────────────────────────────────────────── +[program:scheduler] +command=bash -c "while true; do php /var/www/html/artisan schedule:run --no-ansi >> /proc/1/fd/1 2>&1; sleep 60; done" +autostart=true +autorestart=true +priority=30 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/resources/views/admin/update.blade.php b/resources/views/admin/update.blade.php new file mode 100644 index 0000000..2315b01 --- /dev/null +++ b/resources/views/admin/update.blade.php @@ -0,0 +1,142 @@ + + +

Software-Update

+
+ +
+
+ + {{-- Ergebnis einer abgeschlossenen Installation --}} + @if(session('update_result') === 'success') +
+

✓ Update erfolgreich installiert!

+ @if(session('update_log')) +
{{ session('update_log') }}
+ @endif +
+ @elseif(session('update_result') === 'error') +
+

✗ Update fehlgeschlagen

+ @if(session('update_log')) +
{{ session('update_log') }}
+ @endif +
+ @endif + + {{-- Versions-Info -----------------------------------------------}} +
+
+ Versionsinformationen +
+
+
+ Installierte Version + {{ $currentVersion }} +
+
+ Neueste Version (Gitea) + @if($error) + nicht abrufbar + @elseif($latestVersion) + + {{ $latestVersion }} + + @else + + @endif +
+
+ Gitea-Repository + + @if($giteaUrl) + {{ $giteaRepo }} + @else + nicht konfiguriert + @endif + +
+ @if($checkedAt) +
+ Zuletzt geprüft + {{ \Carbon\Carbon::parse($checkedAt)->format('d.m.Y H:i') }} +
+ @endif +
+
+ + {{-- Fehler-Box --}} + @if($error) +
+

⚠ Update-Prüfung nicht möglich

+

{{ $error }}

+ @if(!$giteaUrl) +

In der .env eintragen:

+
GITEA_URL=http://<IP-des-Gitea-Servers>:3000
+GITEA_REPO=admin/Network-MGMT
+ @endif +
+ @endif + + {{-- Update verfügbar -------------------------------------------}} + @if($updateAvailable) +
+
+ + 🆕 Update verfügbar: {{ $currentVersion }} → {{ $latestVersion }} + +
+
+ @if($releaseNotes) +
{{ $releaseNotes }}
+ @endif + +
+ @csrf + + +
+

+ Die App geht kurz in den Wartungsmodus. Laufende Anfragen werden danach abgeschlossen. +

+
+
+ @elseif(!$error && $latestVersion) +
+ ✓ Du verwendest bereits die neueste Version ({{ $currentVersion }}). +
+ @endif + + {{-- Manuelle Aktualisierung -----------------------------------}} +
+
+ Manuell aktualisieren +
+
+

Um einen bestimmten Tag direkt zu installieren:

+
+ @csrf +
+ + +
+
+

+ Alternativ per Terminal: php artisan app:install-update +

+
+
+ +
+
+
diff --git a/resources/views/errors/maintenance.blade.php b/resources/views/errors/maintenance.blade.php new file mode 100644 index 0000000..c1227a4 --- /dev/null +++ b/resources/views/errors/maintenance.blade.php @@ -0,0 +1,26 @@ + + + + + + Update läuft – bitte warten + + + +
+
+

🔄 Update wird installiert

+

Bitte einen Moment Geduld.
Die Seite lädt automatisch neu.

+
+ + diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 40f8c97..2cbc0a4 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -23,6 +23,11 @@