Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2aa1eaf5 |
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
];
|
||||||
@@ -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 ""
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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://<IP-des-Gitea-Servers>: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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user