2 Commits

Author SHA1 Message Date
gitea-mms 3dc9c2c42d v0.10.0: deploy.sh via Gitea 2026-07-03 01:04:24 +02:00
gitea-mms af2aa1eaf5 v0.10.0: Docker + Update-Funktion + deploy.sh 2026-07-02 21:14:18 +02:00
20 changed files with 1122 additions and 11 deletions
+34
View File
@@ -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
+18 -10
View File
@@ -1,8 +1,9 @@
APP_NAME=Laravel APP_NAME="Network-MGMT"
APP_ENV=local APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=false
APP_URL=http://localhost APP_URL=http://localhost:8080
APP_PORT=8080
APP_LOCALE=en APP_LOCALE=en
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
@@ -20,12 +21,13 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=mysql
# DB_HOST=127.0.0.1 DB_HOST=db
# DB_PORT=3306 DB_PORT=3306
# DB_DATABASE=laravel DB_DATABASE=network_mgmt
# DB_USERNAME=root DB_USERNAME=network_mgmt
# DB_PASSWORD= DB_PASSWORD=secret
DB_ROOT_PASSWORD=rootsecret
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@@ -63,3 +65,9 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" 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
+39 -1
View File
@@ -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 <tag>`, `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://<IP-des-Gitea-Servers>:3000
GITEA_REPO=admin/Network-MGMT
```
In `.env` eintragen, danach steht der Update-Check zur Verfügung.
### Docker-Deployment
```bash
git clone http://<gitea>: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 ## [0.9.0] - 2026-07-02
### Added ### 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 - 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 - 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: `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) ### Setup (einmalig)
``` ```
php artisan migrate php artisan migrate
php artisan network:backfill-devices
``` ```
--- ---
@@ -152,7 +189,8 @@ php artisan migrate
- Grundlegende PHP-Projektstruktur (public/, src/, config/) - Grundlegende PHP-Projektstruktur (public/, src/, config/)
- composer.json, .gitignore, README.md - 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.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.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 [0.7.0]: http://localhost:3000/admin/Network-MGMT/compare/v0.6.0...v0.7.0
+45
View File
@@ -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"]
@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class AppCheckUpdateCommand extends Command
{
protected $signature = 'app:check-update {--json : Ausgabe als JSON}';
protected $description = 'Prüft ob ein Update auf Gitea verfügbar ist und speichert das Ergebnis im Cache';
public function handle(): int
{
$current = config('version.current');
$giteaUrl = rtrim(config('version.gitea_url'), '/');
$repo = config('version.gitea_repo');
if (empty($giteaUrl)) {
$this->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;
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
class AppInstallUpdateCommand extends Command
{
protected $signature = 'app:install-update {--tag= : Bestimmten Git-Tag installieren}';
protected $description = 'Installiert das neueste Update vom Gitea-Repository';
public function handle(): int
{
$projectPath = base_path();
$this->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}");
}
}
}
}
}
@@ -0,0 +1,132 @@
<?php
namespace App\Console\Commands;
use App\Models\NetworkDevice;
use App\Models\NetworkHost;
use Illuminate\Console\Command;
class NetworkBackfillDevicesCommand extends Command
{
protected $signature = 'network:backfill-devices
{--dry-run : Nur anzeigen, was gemacht würde, ohne zu schreiben}';
protected $description = 'Erstellt NetworkDevice-Einträge für bestehende network_hosts ohne device_id';
public function handle(): int
{
$dryRun = $this->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;
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\View\View;
class UpdateController extends Controller
{
public function index(): View
{
$currentVersion = config('version.current');
$giteaUrl = rtrim(config('version.gitea_url'), '/');
$giteaRepo = config('version.gitea_repo');
$latestVersion = null;
$updateAvailable = false;
$releaseNotes = null;
$error = null;
$checkedAt = Cache::get('app_update_checked_at');
if (empty($giteaUrl)) {
$error = 'GITEA_URL ist nicht in der .env konfiguriert. Bitte eintragen und Container neu starten.';
} else {
// Frisch prüfen (Cache überschreiben bei manuellem Aufruf)
try {
$response = Http::timeout(8)
->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,
]);
}
}
@@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use App\Services\SettingsService; use App\Services\SettingsService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@@ -29,5 +30,13 @@ class SettingsServiceProvider extends ServiceProvider
'theme_mode' => 'light', '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);
}
} }
} }
+21
View File
@@ -0,0 +1,21 @@
<?php
return [
/*
* Aktuelle App-Version wird bei jedem Update automatisch angepasst.
* Muss dem Git-Tag des letzten Releases entsprechen (z.B. "v0.9.0").
*/
'current' => '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'),
];
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# Network-MGMT — Deploy-Skript
#
# Ausführen auf deinem lokalen Rechner: bash deploy.sh
# Der Zielserver klont die App direkt von Gitea.
#
# Voraussetzung: SSH-Zugang zum Zielserver
# ═══════════════════════════════════════════════════════════════════
set -euo pipefail
G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
echo ""
echo -e "${B}${BOLD}╔═══════════════════════════════════════════╗${NC}"
echo -e "${B}${BOLD}║ Network-MGMT — Deployment ║${NC}"
echo -e "${B}${BOLD}╚═══════════════════════════════════════════╝${NC}"
echo ""
# ── Eingaben ───────────────────────────────────────────────────────
echo -e "${BOLD}Kunden-Server (Ziel)${NC}"
read -rp " IP-Adresse / 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}"
echo ""
echo -e "${BOLD}Gitea${NC}"
read -rp " Gitea-URL (dein Server) : " GITEA_URL; GITEA_URL="${GITEA_URL:-https://git.mms-systemservice.de}"
read -rp " Gitea-Benutzer : " GITEA_USER; GITEA_USER="${GITEA_USER:-gitea-mms}"
read -rsp " Gitea-Passwort : " GITEA_PASS; echo
echo ""
echo -e "${BOLD}App-Einstellungen${NC}"
read -rp " App-Port [8080] : " APP_PORT; APP_PORT="${APP_PORT:-8080}"
APP_URL="http://${SERVER_HOST}:${APP_PORT}"
echo -e " → Erreichbar unter: ${G}${APP_URL}${NC}"
echo ""
echo -e "${BOLD}Datenbank${NC}"
read -rsp " DB-Passwort : " DB_PASSWORD; echo
DB_ROOT_PASSWORD="${DB_PASSWORD}ROOT"
# ── Bestätigung ────────────────────────────────────────────────────
echo ""
echo -e "${Y}────────────────────────────────────────────────${NC}"
echo " Ziel: ${SERVER_USER}@${SERVER_HOST}:${SERVER_PORT}"
echo " Quelle: ${GITEA_URL}/${GITEA_USER}/Network-MGMT"
echo " App-URL: ${APP_URL}"
echo -e "${Y}────────────────────────────────────────────────${NC}"
echo ""
read -rp "Deployment starten? [j/N] " CONFIRM
[[ "${CONFIRM,,}" == "j" ]] || { echo "Abgebrochen."; exit 0; }
# ── Setup-Skript mit eingebetteten Werten erzeugen ─────────────────
REMOTE_SCRIPT="/tmp/nm_deploy_$$.sh"
{
printf '#!/bin/bash\nset -euo pipefail\n\n'
printf 'G='"'"'\033[0;32m'"'"'; Y='"'"'\033[1;33m'"'"'; BOLD='"'"'\033[1m'"'"'; NC='"'"'\033[0m'"'"'\n\n'
printf 'GITEA_URL=%q\n' "$GITEA_URL"
printf 'GITEA_USER=%q\n' "$GITEA_USER"
printf 'GITEA_PASS=%q\n' "$GITEA_PASS"
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_FULL_URL=%q\n' "${GITEA_URL}"
cat << 'SETUP'
DEPLOY_PATH="/opt/network-mgmt"
REPO_URL="${GITEA_URL/https:\/\//https://${GITEA_USER}:${GITEA_PASS}@}"
REPO_URL="${REPO_URL}/gitea-mms/Network-MGMT.git"
# Credentials in URL einbauen
CLONE_URL=$(echo "$GITEA_URL" | sed "s|https://|https://${GITEA_USER}:${GITEA_PASS}@|")
CLONE_URL="${CLONE_URL}/${GITEA_USER}/Network-MGMT.git"
echo ""
echo -e "${Y}[1/4] Docker prüfen / installieren ...${NC}"
if ! command -v docker &>/dev/null; then
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
fi
echo -e "${G} ✓ Docker: $(docker --version | awk '{print $3}' | tr -d ',')${NC}"
if ! docker compose version &>/dev/null 2>&1; then
ARCH="$(uname -m)"
mkdir -p "${HOME}/.docker/cli-plugins"
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)${NC}"
echo ""
echo -e "${Y}[2/4] Repository von Gitea klonen / aktualisieren ...${NC}"
if [ -d "${DEPLOY_PATH}/.git" ]; then
cd "$DEPLOY_PATH"
git pull
echo -e "${G} ✓ Repository aktualisiert${NC}"
else
git clone "$CLONE_URL" "$DEPLOY_PATH"
echo -e "${G} ✓ Repository geklont${NC}"
fi
echo ""
echo -e "${Y}[3/4] Konfiguration ...${NC}"
cd "$DEPLOY_PATH"
mkdir -p storage/logs storage/framework/{cache,sessions,views} bootstrap/cache
chmod -R 775 storage bootstrap/cache
if [ ! -f ".env" ]; then
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
sed -i "s|GITEA_URL=.*|GITEA_URL=${GITEA_FULL_URL}|" .env
sed -i "s|GITEA_REPO=.*|GITEA_REPO=${GITEA_USER}/Network-MGMT|" .env
echo -e "${G} ✓ .env erstellt${NC}"
else
echo -e "${G} ✓ .env bereits vorhanden (nicht überschrieben)${NC}"
fi
echo ""
echo -e "${Y}[4/4] Container bauen und starten ...${NC}"
echo " (Erster Start: 35 Minuten)"
docker compose up -d --build
echo " Warte auf Start ..."
sleep 8
for i in $(seq 1 20); do
docker compose ps | grep -qE "healthy|Up" && break || sleep 5
done
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}║ Browser: %-40s║${NC}\n" "${APP_URL}"
printf "${G}${BOLD}║ Pfad: %-40s║${NC}\n" "${DEPLOY_PATH}"
echo -e "${G}${BOLD}║ ║${NC}"
echo -e "${G}${BOLD}║ Logs: docker compose logs -f ║${NC}"
echo -e "${G}${BOLD}║ Stop: docker compose down ║${NC}"
echo -e "${G}${BOLD}╚══════════════════════════════════════════════════╝${NC}"
echo ""
SETUP
} > "$REMOTE_SCRIPT"
chmod +x "$REMOTE_SCRIPT"
# ── Script auf Server übertragen und ausführen ─────────────────────
echo ""
echo -e "${Y}▶ Verbinde mit ${SERVER_USER}@${SERVER_HOST} ...${NC}"
scp -q -P "$SERVER_PORT" "$REMOTE_SCRIPT" "${SERVER_USER}@${SERVER_HOST}:/tmp/nm_deploy.sh"
rm -f "$REMOTE_SCRIPT"
echo -e "${Y}▶ Setup läuft (Ausgabe vom Server):${NC}"
echo -e "${Y}────────────────────────────────────────────────${NC}"
ssh -p "$SERVER_PORT" "${SERVER_USER}@${SERVER_HOST}" \
"bash /tmp/nm_deploy.sh; rm -f /tmp/nm_deploy.sh"
echo -e "${Y}────────────────────────────────────────────────${NC}"
echo ""
echo -e "${G}${BOLD}✓ Fertig! → ${APP_URL}${NC}"
echo ""
+48
View File
@@ -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:
+80
View File
@@ -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
+43
View File
@@ -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;
}
}
+38
View File
@@ -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
+142
View File
@@ -0,0 +1,142 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200">Software-Update</h2>
</x-slot>
<div class="py-8">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8 space-y-5">
{{-- Ergebnis einer abgeschlossenen Installation --}}
@if(session('update_result') === 'success')
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-lg p-4">
<p class="font-semibold text-green-800 dark:text-green-300"> Update erfolgreich installiert!</p>
@if(session('update_log'))
<pre class="mt-2 text-xs text-green-700 dark:text-green-400 overflow-auto max-h-40">{{ session('update_log') }}</pre>
@endif
</div>
@elseif(session('update_result') === 'error')
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-lg p-4">
<p class="font-semibold text-red-800 dark:text-red-300"> Update fehlgeschlagen</p>
@if(session('update_log'))
<pre class="mt-2 text-xs text-red-700 dark:text-red-400 overflow-auto max-h-40">{{ session('update_log') }}</pre>
@endif
</div>
@endif
{{-- Versions-Info -----------------------------------------------}}
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Versionsinformationen</span>
</div>
<div class="p-4 space-y-3 text-sm">
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Installierte Version</span>
<span class="font-mono font-semibold text-gray-900 dark:text-gray-100">{{ $currentVersion }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Neueste Version (Gitea)</span>
@if($error)
<span class="text-red-500 text-xs">nicht abrufbar</span>
@elseif($latestVersion)
<span class="font-mono font-semibold {{ $updateAvailable ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400' }}">
{{ $latestVersion }}
</span>
@else
<span class="text-gray-400 text-xs"></span>
@endif
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Gitea-Repository</span>
<span class="font-mono text-xs text-gray-500 dark:text-gray-400">
@if($giteaUrl)
<a href="{{ $giteaUrl }}/{{ $giteaRepo }}" target="_blank"
class="text-indigo-600 hover:underline">{{ $giteaRepo }}</a>
@else
nicht konfiguriert
@endif
</span>
</div>
@if($checkedAt)
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Zuletzt geprüft</span>
<span class="text-xs text-gray-400">{{ \Carbon\Carbon::parse($checkedAt)->format('d.m.Y H:i') }}</span>
</div>
@endif
</div>
</div>
{{-- Fehler-Box --}}
@if($error)
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-4 text-sm text-amber-800 dark:text-amber-300">
<p class="font-semibold"> Update-Prüfung nicht möglich</p>
<p class="mt-1 text-xs">{{ $error }}</p>
@if(!$giteaUrl)
<p class="mt-2 text-xs">In der <code class="bg-amber-100 dark:bg-amber-900 px-1 rounded">.env</code> eintragen:</p>
<pre class="mt-1 text-xs bg-amber-100 dark:bg-amber-900 rounded p-2">GITEA_URL=http://&lt;IP-des-Gitea-Servers&gt;:3000
GITEA_REPO=admin/Network-MGMT</pre>
@endif
</div>
@endif
{{-- Update verfügbar -------------------------------------------}}
@if($updateAvailable)
<div class="bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-200 dark:border-indigo-700 rounded-lg overflow-hidden">
<div class="px-4 py-3 bg-indigo-100 dark:bg-indigo-900/40 border-b border-indigo-200 dark:border-indigo-700 flex items-center justify-between">
<span class="font-semibold text-indigo-800 dark:text-indigo-300">
🆕 Update verfügbar: {{ $currentVersion }} {{ $latestVersion }}
</span>
</div>
<div class="p-4">
@if($releaseNotes)
<div class="text-sm text-gray-700 dark:text-gray-300 mb-4 max-h-48 overflow-y-auto bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono whitespace-pre-wrap">{{ $releaseNotes }}</div>
@endif
<form method="POST" action="{{ route('admin.update.install') }}"
onsubmit="return confirm('Update {{ $latestVersion }} jetzt installieren?\n\nDie App geht kurz in den Wartungsmodus.')">
@csrf
<input type="hidden" name="tag" value="{{ $latestVersion }}">
<button type="submit"
style="background-color: var(--color-primary)"
class="px-5 py-2 text-white text-sm font-semibold rounded-md hover:opacity-90 transition">
Update {{ $latestVersion }} jetzt installieren
</button>
</form>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Die App geht kurz in den Wartungsmodus. Laufende Anfragen werden danach abgeschlossen.
</p>
</div>
</div>
@elseif(!$error && $latestVersion)
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700 rounded-lg p-4 text-sm text-green-800 dark:text-green-300">
Du verwendest bereits die neueste Version ({{ $currentVersion }}).
</div>
@endif
{{-- Manuelle Aktualisierung -----------------------------------}}
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Manuell aktualisieren</span>
</div>
<div class="p-4 space-y-3 text-sm text-gray-600 dark:text-gray-400">
<p>Um einen bestimmten Tag direkt zu installieren:</p>
<form method="POST" action="{{ route('admin.update.install') }}"
onsubmit="return confirm('Wirklich auf diesen Tag aktualisieren?')">
@csrf
<div class="flex gap-2">
<input type="text" name="tag" placeholder="z.B. v0.10.0"
class="flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500" />
<button type="submit"
class="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
Installieren
</button>
</div>
</form>
<p class="text-xs text-gray-400">
Alternativ per Terminal: <code class="bg-gray-100 dark:bg-gray-900 px-1 rounded">php artisan app:install-update</code>
</p>
</div>
</div>
</div>
</div>
</x-app-layout>
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="10">
<title>Update läuft bitte warten</title>
<style>
body { font-family: Arial, sans-serif; display: flex; align-items: center; justify-content: center;
height: 100vh; margin: 0; background: #f3f4f6; color: #1f2937; }
.box { text-align: center; padding: 2rem; background: white; border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,.1); max-width: 400px; }
h1 { font-size: 1.5rem; margin-bottom: .5rem; }
p { color: #6b7280; font-size: .9rem; }
.spinner { width: 40px; height: 40px; border: 4px solid #e5e7eb; border-top-color: #6366f1;
border-radius: 50%; animation: spin 1s linear infinite; margin: 1rem auto; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="box">
<div class="spinner"></div>
<h1>🔄 Update wird installiert</h1>
<p>Bitte einen Moment Geduld.<br>Die Seite lädt automatisch neu.</p>
</div>
</body>
</html>
@@ -23,6 +23,11 @@
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none <button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}"> {{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
Einstellungen Einstellungen
@if($navUpdateAvailable)
<span class="ml-1.5 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-semibold bg-amber-400 text-amber-900">
Update
</span>
@endif
<svg class="ms-1 fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <svg class="ms-1 fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
@@ -35,6 +40,12 @@
<x-dropdown-link :href="route('admin.layout.index')"> <x-dropdown-link :href="route('admin.layout.index')">
🎨 Layout 🎨 Layout
</x-dropdown-link> </x-dropdown-link>
<x-dropdown-link :href="route('admin.update.index')">
🔄 Software-Update
@if($navUpdateAvailable)
<span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-semibold bg-amber-400 text-amber-900">neu</span>
@endif
</x-dropdown-link>
</x-slot> </x-slot>
</x-dropdown> </x-dropdown>
@endrole @endrole
+5
View File
@@ -15,3 +15,8 @@ Schedule::command('network:scan')
->everyMinute() ->everyMinute()
->withoutOverlapping() ->withoutOverlapping()
->runInBackground(); ->runInBackground();
// Update-Prüfung alle 6 Stunden
Schedule::command('app:check-update')
->everySixHours()
->runInBackground();
+5
View File
@@ -6,6 +6,7 @@ use App\Http\Controllers\NetworkController;
use App\Http\Controllers\NetworkSegmentController; use App\Http\Controllers\NetworkSegmentController;
use App\Http\Controllers\Admin\UserController as AdminUserController; use App\Http\Controllers\Admin\UserController as AdminUserController;
use App\Http\Controllers\Admin\LayoutController as AdminLayoutController; use App\Http\Controllers\Admin\LayoutController as AdminLayoutController;
use App\Http\Controllers\Admin\UpdateController as AdminUpdateController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
@@ -32,6 +33,10 @@ Route::prefix('admin')
Route::get('layout', [AdminLayoutController::class, 'index'])->name('layout.index'); Route::get('layout', [AdminLayoutController::class, 'index'])->name('layout.index');
Route::put('layout', [AdminLayoutController::class, 'update'])->name('layout.update'); Route::put('layout', [AdminLayoutController::class, 'update'])->name('layout.update');
Route::get('layout/remove-logo', [AdminLayoutController::class, 'removeLogo'])->name('layout.removeLogo'); Route::get('layout/remove-logo', [AdminLayoutController::class, 'removeLogo'])->name('layout.removeLogo');
// Software-Update
Route::get('update', [AdminUpdateController::class, 'index'])->name('update.index');
Route::post('update/install', [AdminUpdateController::class, 'install'])->name('update.install');
}); });
// Netzwerk-Bereich für alle eingeloggten Benutzer // Netzwerk-Bereich für alle eingeloggten Benutzer