You've already forked Atomcms-edit
Compare commits
50 Commits
769abddeca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1db80e76fe | |||
| 51e3876ed9 | |||
| 483342602f | |||
| ccbd200464 | |||
| 834908df9f | |||
| 434f58d5aa | |||
| 4c085ac7eb | |||
| bbefdce290 | |||
| 5b8d1cda7d | |||
| ea2160132a | |||
| 2ac1264b93 | |||
| 0f0de43da5 | |||
| 1ee60405ce | |||
| 78704190bc | |||
| f109b1f764 | |||
| af2f76d9db | |||
| 6f11cae3ca | |||
| 16b2f5bbb7 | |||
| 6e437be6d1 | |||
| 4f4f40ac99 | |||
| be6a578f5e | |||
| 4d8d22f40a | |||
| 8a324b3082 | |||
| f7fe86efeb | |||
| 36887244e6 | |||
| 9b5c655c68 | |||
| b2bb1811d0 | |||
| 4b6872e5e0 | |||
| 66cbd46f37 | |||
| 889cec60e9 | |||
| 8ce3fcca85 | |||
| b8a15c8412 | |||
| 82d00ad11d | |||
| c468040792 | |||
| 4487084614 | |||
| e96e2a0fd3 | |||
| 4378144f45 | |||
| 59188a5f2c | |||
| ff364992ca | |||
| 2997486662 | |||
| f76f30e88f | |||
| 1f04979ffe | |||
| 1f9af5279a | |||
| 7814176358 | |||
| 56616d084c | |||
| e34300a8a1 | |||
| e2422e9a16 | |||
| a65db47c85 | |||
| af2c79ac59 | |||
| e754858c84 |
-113
@@ -1,113 +0,0 @@
|
|||||||
# AtomCMS Environment Configuration
|
|
||||||
# Works on Linux (Nginx/Apache) and Windows (XAMPP/IIS)
|
|
||||||
# Copy to .env and adjust values for your setup
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# APPLICATION
|
|
||||||
# ==========================================
|
|
||||||
APP_NAME="AtomCMS"
|
|
||||||
APP_ENV=local
|
|
||||||
APP_KEY=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# DATABASE - Works on Linux & Windows
|
|
||||||
# ==========================================
|
|
||||||
DB_CONNECTION=mariadb
|
|
||||||
DB_HOST=127.0.0.1 # Linux: 127.0.0.1, Windows XAMPP: localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=habbo
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
|
|
||||||
# Windows IIS (SQL Server) - uncomment if using IIS/SQL Server:
|
|
||||||
# DB_CONNECTION=sqlsrv
|
|
||||||
# DB_HOST=(local)
|
|
||||||
# DB_DATABASE=habbo
|
|
||||||
# DB_USERNAME=sa
|
|
||||||
# DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# SESSION & CACHE (Windows compatible)
|
|
||||||
# ==========================================
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
CACHE_DRIVER=file
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# MAIL (Windows compatible)
|
|
||||||
# ==========================================
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_HOST=smtp.mailtrap.io
|
|
||||||
MAIL_PORT=2525
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=tls
|
|
||||||
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# REAL-TIME & PUSHER
|
|
||||||
# ==========================================
|
|
||||||
PUSHER_APP_ID=
|
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_HOST=
|
|
||||||
PUSHER_PORT=443
|
|
||||||
PUSHER_SCHEME=https
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# SECURITY
|
|
||||||
# ==========================================
|
|
||||||
# Cloudflare Turnstile (optional)
|
|
||||||
TURNSTILE_SITE_KEY=
|
|
||||||
TURNSTILE_SECRET_KEY=
|
|
||||||
|
|
||||||
# Google Recaptcha (optional)
|
|
||||||
NOCAPTCHA_SECRET=
|
|
||||||
NOCAPTCHA_SITEKEY=
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# WEBHOOKS
|
|
||||||
# ==========================================
|
|
||||||
DISCORD_WEBHOOK_URL=
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# NITRO CLIENT PATHS
|
|
||||||
# ==========================================
|
|
||||||
# Linux: /var/www/atomcms/nitro-client
|
|
||||||
# Windows: C:\path\to\atomcms\nitro-client
|
|
||||||
NITRO_CLIENT_PATH=/var/www/atomcms/nitro-client
|
|
||||||
NITRO_RENDERER_PATH=/var/www/atomcms/nitro-renderer
|
|
||||||
NITRO_BUILD_PATH=/var/www/atomcms/nitro-client/dist
|
|
||||||
NITRO_WEBROOT=/Client
|
|
||||||
GAMEDATA_PATH=/var/Gamedata
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# LOGGING
|
|
||||||
# ==========================================
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# BROADCAST & FILESYSTEMS
|
|
||||||
# ==========================================
|
|
||||||
BROADCAST_DRIVER=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# LOCALIZATION
|
|
||||||
# ==========================================
|
|
||||||
APP_LOCALE=nl
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
APP_FAKER_LOCALE=nl_NL
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# SECURITY SETTINGS
|
|
||||||
# ==========================================
|
|
||||||
APP_SECURE=true
|
|
||||||
SESSION_DOMAIN=localhost
|
|
||||||
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
APP_NAME="YourHotel"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://yourhotel.nl
|
||||||
|
|
||||||
|
# --- LOGGING ---
|
||||||
|
LOG_CHANNEL=daily
|
||||||
|
LOG_MAX_FILES=14
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
# --- DATABASE ---
|
||||||
|
DB_CONNECTION=mariadb
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=habbo
|
||||||
|
DB_USERNAME=cms
|
||||||
|
DB_PASSWORD=your_db_password
|
||||||
|
DB_STRICT_MODE=false
|
||||||
|
DB_ENGINE=InnoDB
|
||||||
|
DB_CHARSET=utf8mb4
|
||||||
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
|
DB_DUMP_BINARY_PATH=/usr/bin/
|
||||||
|
|
||||||
|
# --- CACHE & SESSION (Redis recommended on Linux) ---
|
||||||
|
BROADCAST_DRIVER=redis
|
||||||
|
CACHE_STORE=redis
|
||||||
|
CACHE_DRIVER=redis
|
||||||
|
SESSION_DRIVER=redis
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_CACHE_DB=1
|
||||||
|
REDIS_QUEUE_DB=2
|
||||||
|
|
||||||
|
REDIS_PERSISTENT=true
|
||||||
|
REDIS_READ_TIMEOUT=1.0
|
||||||
|
REDIS_CONNECT_TIMEOUT=1.0
|
||||||
|
|
||||||
|
# --- FILAMENT ---
|
||||||
|
FILAMENT_FILESYSTEM_DISK=public
|
||||||
|
FILAMENT_AUTH_GUARD=web
|
||||||
|
|
||||||
|
# --- MAIL ---
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# --- SESSION SECURITY ---
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_DOMAIN=.yourhotel.nl
|
||||||
|
SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
SESSION_COOKIE=__secure_session
|
||||||
|
|
||||||
|
# --- SANCTUM ---
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=yourhotel.nl,www.yourhotel.nl,localhost:3000
|
||||||
|
|
||||||
|
# --- EMULATOR ---
|
||||||
|
TURNSTILE_SITE_KEY=
|
||||||
|
TURNSTILE_SECRET_KEY=
|
||||||
|
RCON_HOST=127.0.0.1
|
||||||
|
RCON_PORT=3001
|
||||||
|
EMULATOR_IP=127.0.0.1
|
||||||
|
EMULATOR_PORT=3000
|
||||||
|
|
||||||
|
# --- NITRO UPDATE (for update-Nitrov3.sh) ---
|
||||||
|
NITRO_EMULATOR_PATH=/var/www/emulator
|
||||||
|
NITRO_EMULATOR_SERVICE=emulator
|
||||||
|
NITRO_DB_HOST=127.0.0.1
|
||||||
|
NITRO_DB_PORT=3306
|
||||||
|
NITRO_DB_NAME=habbo
|
||||||
|
NITRO_DB_USER=root
|
||||||
|
NITRO_DB_PASS=
|
||||||
|
NITRO_SQL_DIR=/var/www/emulator/Database Updates
|
||||||
|
NITRO_BACKUP_DIR=/var/www/emulator/Database Updates/backups
|
||||||
|
NITRO_GAMEDATA_DIR=/var/www/Gamedata/config
|
||||||
|
NITRO_CLIENT_DIR=/var/www/Nitro-V3/public/configuration
|
||||||
|
NITRO_CLIENT_SRC=/var/www/Nitro-V3
|
||||||
|
NITRO_RENDERER_SRC=/var/www/Nitro_Render_V3
|
||||||
|
|
||||||
|
# --- CORS ---
|
||||||
|
CORS_ALLOWED_ORIGINS=https://yourhotel.nl,https://www.yourhotel.nl,http://localhost:3000
|
||||||
|
|
||||||
|
# --- LOCALIZATION ---
|
||||||
|
APP_LOCALE=nl
|
||||||
|
FORCE_HTTPS=true
|
||||||
|
PASSWORD_RESET_TOKEN_TIME=15
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
APP_NAME="YourHotel"
|
||||||
|
APP_ENV=production
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
# --- LOGGING ---
|
||||||
|
LOG_CHANNEL=daily
|
||||||
|
LOG_MAX_FILES=14
|
||||||
|
LOG_LEVEL=error
|
||||||
|
|
||||||
|
# --- DATABASE ---
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=habbo
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_STRICT_MODE=false
|
||||||
|
DB_ENGINE=InnoDB
|
||||||
|
DB_CHARSET=utf8mb4
|
||||||
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
|
|
||||||
|
# --- CACHE & SESSION (File-based for Windows compat) ---
|
||||||
|
BROADCAST_DRIVER=log
|
||||||
|
CACHE_STORE=file
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
SESSION_DRIVER=file
|
||||||
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
|
# --- MAIL ---
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=smtp.mailtrap.io
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS="noreply@yourhotel.nl"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
# --- SESSION SECURITY ---
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_DOMAIN=localhost
|
||||||
|
SESSION_SECURE_COOKIE=false
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
SESSION_COOKIE=__secure_session
|
||||||
|
|
||||||
|
# --- EMULATOR ---
|
||||||
|
TURNSTILE_SITE_KEY=
|
||||||
|
TURNSTILE_SECRET_KEY=
|
||||||
|
RCON_HOST=127.0.0.1
|
||||||
|
RCON_PORT=3001
|
||||||
|
EMULATOR_IP=127.0.0.1
|
||||||
|
EMULATOR_PORT=3000
|
||||||
|
|
||||||
|
# --- CORS ---
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
|
# --- LOCALIZATION ---
|
||||||
|
APP_LOCALE=nl
|
||||||
|
FORCE_HTTPS=false
|
||||||
|
PASSWORD_RESET_TOKEN_TIME=15
|
||||||
|
|
||||||
|
# --- NITRO PATHS (Windows) ---
|
||||||
|
NITRO_CLIENT_PATH=C:\Nitro-V3
|
||||||
|
NITRO_RENDERER_PATH=C:\Nitro_Render_V3
|
||||||
|
NITRO_BUILD_PATH=C:\Nitro-V3\dist
|
||||||
|
NITRO_WEBROOT=/Client
|
||||||
|
GAMEDATA_PATH=C:\Gamedata
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
APP_NAME="Epicnabbo Hotel"
|
|
||||||
APP_ENV=production
|
|
||||||
APP_KEY=base64:YOUR_APP_KEY_HERE
|
|
||||||
APP_DEBUG=false
|
|
||||||
APP_URL=https://epicnabbo.nl
|
|
||||||
|
|
||||||
# --- LOGGING ---
|
|
||||||
LOG_CHANNEL=daily
|
|
||||||
LOG_MAX_FILES=14
|
|
||||||
LOG_LEVEL=error
|
|
||||||
|
|
||||||
# --- DATABASE ---
|
|
||||||
DB_CONNECTION=mariadb
|
|
||||||
DB_HOST=YOUR_DB_HOST
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=YOUR_DB_NAME
|
|
||||||
DB_USERNAME=YOUR_DB_USER
|
|
||||||
DB_PASSWORD=YOUR_DB_PASSWORD
|
|
||||||
DB_STRICT_MODE=true
|
|
||||||
DB_ENGINE=InnoDB
|
|
||||||
DB_CHARSET=utf8mb4
|
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
|
||||||
DB_DUMP_BINARY_PATH=/usr/bin/
|
|
||||||
|
|
||||||
# --- REDIS TURBO ---
|
|
||||||
BROADCAST_DRIVER=redis
|
|
||||||
CACHE_STORE=redis
|
|
||||||
CACHE_DRIVER=redis
|
|
||||||
SESSION_DRIVER=redis
|
|
||||||
QUEUE_CONNECTION=redis
|
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
REDIS_DB=0
|
|
||||||
REDIS_CACHE_DB=1
|
|
||||||
REDIS_QUEUE_DB=2
|
|
||||||
|
|
||||||
REDIS_PERSISTENT=true
|
|
||||||
REDIS_READ_TIMEOUT=1.0
|
|
||||||
REDIS_CONNECT_TIMEOUT=1.0
|
|
||||||
|
|
||||||
# --- SESSION SECURITY ---
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_DOMAIN=.epicnabbo.nl
|
|
||||||
SESSION_SECURE_COOKIE=true
|
|
||||||
SESSION_HTTP_ONLY=true
|
|
||||||
SESSION_SAME_SITE=lax
|
|
||||||
SESSION_COOKIE=__epicnabbo_secure_session
|
|
||||||
|
|
||||||
# --- BEVEILIGING & EMULATOR ---
|
|
||||||
TURNSTILE_SITE_KEY=YOUR_TURNSTILE_SITE_KEY
|
|
||||||
TURNSTILE_SECRET_KEY=YOUR_TURNSTILE_SECRET_KEY
|
|
||||||
RCON_HOST=127.0.0.1
|
|
||||||
RCON_PORT=3001
|
|
||||||
EMULATOR_IP=127.0.0.1
|
|
||||||
EMULATOR_PORT=3000
|
|
||||||
|
|
||||||
# --- EXTRA HARDENING ---
|
|
||||||
APP_LOCALE=nl
|
|
||||||
FORCE_HTTPS=true
|
|
||||||
PASSWORD_RESET_TOKEN_TIME=15
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Windows/XAMPP/IIS .env configuration
|
|
||||||
# Copy this to .env and adjust the values for your Windows setup
|
|
||||||
|
|
||||||
# Application
|
|
||||||
APP_NAME="Epicnabbo"
|
|
||||||
APP_ENV=local
|
|
||||||
APP_KEY=base64:EXAMPLE_KEY_CHANGE_ME
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
|
|
||||||
# Database - Windows/XAMPP Settings
|
|
||||||
# For XAMPP use: DB_HOST=localhost
|
|
||||||
# For IIS use: DB_HOST=127.0.0.1 or localhost
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=habbo
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
|
|
||||||
# For Windows IIS with named pipes, use:
|
|
||||||
# DB_CONNECTION=sqlsrv
|
|
||||||
# DB_HOST=(local)
|
|
||||||
# DB_DATABASE=habbo
|
|
||||||
# DB_USERNAME=sa
|
|
||||||
# DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
# Session (Windows compatible)
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
|
|
||||||
# Cache (Windows compatible)
|
|
||||||
CACHE_DRIVER=file
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
|
|
||||||
# Mail (Windows compatible - use Mailtrap for testing)
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_HOST=smtp.mailtrap.io
|
|
||||||
MAIL_PORT=2525
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=tls
|
|
||||||
MAIL_FROM_ADDRESS="noreply@epicnabbo.nl"
|
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
|
||||||
|
|
||||||
# Pusher (for real-time features)
|
|
||||||
PUSHER_APP_ID=
|
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_HOST=
|
|
||||||
PUSHER_PORT=443
|
|
||||||
PUSHER_SCHEME=https
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
# Cloudflare Turnstile (optional)
|
|
||||||
TURNSTILE_SITE_KEY=
|
|
||||||
TURNSTILE_SECRET_KEY=
|
|
||||||
|
|
||||||
# Google Recaptcha (optional)
|
|
||||||
NOCAPTCHA_SECRET=
|
|
||||||
NOCAPTCHA_SITEKEY=
|
|
||||||
|
|
||||||
# Discord Webhook (optional)
|
|
||||||
DISCORD_WEBHOOK_URL=
|
|
||||||
|
|
||||||
# Nitro Client Path (Windows paths use backslashes or forward slashes)
|
|
||||||
NITRO_CLIENT_PATH=C:\path\to\nitro-client
|
|
||||||
NITRO_RENDERER_PATH=C:\path\to\nitro-renderer
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# Broadcast & Filesystems (Windows compatible)
|
|
||||||
BROADCAST_DRIVER=log
|
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
|
|
||||||
# Advanced (usually no changes needed)
|
|
||||||
APP_LOCALE=nl
|
|
||||||
APP_FALLBACK_LOCALE=en
|
|
||||||
APP_LOCALE=nl
|
|
||||||
APP_FAKER_LOCALE=nl_NL
|
|
||||||
|
|
||||||
# Security
|
|
||||||
APP_SECURE=true
|
|
||||||
SESSION_DOMAIN=localhost
|
|
||||||
SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
|
|
||||||
+33
-26
@@ -1,42 +1,49 @@
|
|||||||
# Negeer wachtwoorden en database-instellingen
|
# --- Environment ---
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.testing
|
.env.testing
|
||||||
config.php
|
.env.production
|
||||||
wp-config.php
|
|
||||||
/uploads/
|
|
||||||
/temp/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Geen zware afhankelijkheden pushen
|
# --- Dependencies ---
|
||||||
node_modules/
|
node_modules/
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
# UITZONDERING: Vertaalbestanden in lang/vendor wel pushen
|
# --- Build artifacts (hashed filenames) ---
|
||||||
/lang/vendor/
|
public/build/assets/
|
||||||
|
public/build/hot
|
||||||
|
|
||||||
# Systeembestanden (Mac/Windows troep)
|
# --- Storage (uploaded files, cache, logs, sessions) ---
|
||||||
|
storage/app/livewire-tmp/
|
||||||
|
storage/app/public/
|
||||||
|
storage/clockwork/
|
||||||
|
storage/debugbar/
|
||||||
|
storage/framework/views/
|
||||||
|
storage/framework/cache/
|
||||||
|
storage/framework/sessions/
|
||||||
|
storage/framework/testing/
|
||||||
|
storage/logs/
|
||||||
|
|
||||||
|
# --- IDE & OS ---
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# VPS automation scripts (niet pushen naar GitLab)
|
# --- Config (no default .env push) ---
|
||||||
watch.sh
|
config.php
|
||||||
check-updates.sh
|
|
||||||
|
|
||||||
# Cache bestanden (niet pushen naar GitLab)
|
|
||||||
/storage/framework/views/
|
|
||||||
/storage/framework/cache/
|
|
||||||
/storage/framework/sessions/
|
|
||||||
/storage/logs/
|
|
||||||
/storage/debugbar/rr
|
|
||||||
.rr.yaml
|
.rr.yaml
|
||||||
|
|
||||||
# Lockfiles (kies 1 package manager)
|
# --- Tests & Temp ---
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# Overgebleven test/temp bestanden
|
|
||||||
ci_test.txt
|
ci_test.txt
|
||||||
cookies.txt
|
cookies.txt
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
||||||
# GitHub workflows (pushen naar GitLab)
|
# --- Lock files (yarn.lock is tracked, package-lock.json is not) ---
|
||||||
!/.github/workflows/
|
package-lock.json
|
||||||
|
|
||||||
|
# --- Scripts (local-only) ---
|
||||||
|
watch.sh
|
||||||
|
check-updates.sh
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright 2023 ObjectRetros
|
Copyright 2026 Remco (Epicnabbo)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,480 +1,329 @@
|
|||||||
# AtomCMS — Remco Epicnabbo Edition
|
# AtomCMS — Remco Epicnabbo Edition
|
||||||
|
|
||||||
<div align="center">
|
[](https://discord.gg/pP6HyZedAj)
|
||||||
<img src="https://i.imgur.com/9ePNdJ4.png" alt="Atom CMS" width="200"/>
|
[](https://laravel.com)
|
||||||
|
[](https://php.net)
|
||||||
|
[](#)
|
||||||
|
|
||||||
### The Ultimate Retro Hotel CMS
|
A modern Habbo retro CMS powered by Laravel 13, Filament 5, React 19, and Nitro. Forked and maintained by Remco (Epicnabbo).
|
||||||
|
|
||||||
**Modern • Fast • Self-Repairing**
|
---
|
||||||
|
|
||||||
[](https://discord.gg/pP6HyZedAj)
|
## What's New in V3
|
||||||
[](https://laravel.com)
|
|
||||||
[](https://php.net)
|
|
||||||
[](https://github.com/atom-retros/atomcms)
|
|
||||||
|
|
||||||
</div>
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Commandocentrum** | Central admin dashboard with Nitro, emulator & hotel monitoring |
|
||||||
|
| **Nitro V3 Update System** | Auto-update emulator, Nitro client & renderer via CLI (Linux `.env`) |
|
||||||
|
| **Configurable Paths** | 13 paths fully adjustable via `.env` (no database needed) |
|
||||||
|
| **Emulator Control** | Start, stop, restart & check status from the admin panel |
|
||||||
|
| **Live Monitoring** | Online users, emulator status, DB status, server load, diagnostics |
|
||||||
|
| **Hotel Alerts** | Send messages to all online users in real-time |
|
||||||
|
| **Emulator Log Viewer** | Live logs directly in the browser |
|
||||||
|
| **Clothing Sync** | Sync catalog clothing from FigureMap with one click |
|
||||||
|
| **Social Login** | OAuth login via Google, Discord & GitHub |
|
||||||
|
| **Notification Settings** | Email & Discord webhook alerts with rank filtering |
|
||||||
|
| **Staff Activity Log** | Full audit trail of all housekeeping actions |
|
||||||
|
| **Bulletproof Installation** | 12-step guide for Ubuntu 26.04 with Redis, SSL, firewall & PHP tuning |
|
||||||
|
| **PHP 8.5 + Ubuntu 26.04** | Fully compatible with the latest PHP and Ubuntu LTS |
|
||||||
|
| **Dual .env System** | Separate configs for Linux (Redis) and Windows (file-based) |
|
||||||
|
| **XAMPP Blocked** | Explicitly unsupported — we prioritise security |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.your-server.com/remco/Atomcms-edit.git /var/www/atomcms
|
||||||
|
cd /var/www/atomcms
|
||||||
|
cp .env.example.linux .env
|
||||||
|
php artisan key:generate
|
||||||
|
# Edit .env with your DB credentials, then:
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
php artisan migrate --seed
|
||||||
|
yarn install && yarn build:all
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Full installation guide** → `docs/INSTALL.md` or scroll down to [Installation](#installation-ubuntu-2604)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Radio Station
|
| Module | What it does |
|
||||||
- DJ applications & rank system
|
|--------|-------------|
|
||||||
- Live DJ sessions with schedule
|
| **Commandocentrum** | Nitro V3 one-click updater, emulator start/stop/restart, hotel alerts, live monitoring, log viewer, clothing sync, social login (Google/Discord/GitHub) |
|
||||||
- Song requests & voting
|
| **Radio** | DJ apps, live sessions, song requests, shoutbox, leaderboard, contests |
|
||||||
- Shoutbox
|
| **Shop** | Product catalog, virtual currency, vouchers, PayPal |
|
||||||
- Listener points & leaderboard
|
| **Community** | Articles, photo gallery, leaderboard, teams, rare values, badge lottery |
|
||||||
- Contests & giveaways
|
| **Users** | Public profiles, 2FA, referrals, session logs |
|
||||||
- Radio banners & history
|
| **Help** | Ticket system, FAQ, rules |
|
||||||
|
| **Filament Admin** | Users, bans, radio, shop, articles, emulator settings/texts/catalog, chatlogs, word filters, permissions, navigation |
|
||||||
|
| **Themes** | Atom (light) & Dusk (dark) |
|
||||||
|
|
||||||
### Shop
|
---
|
||||||
- Product catalog with categories
|
|
||||||
- Virtual currency purchases
|
|
||||||
- Voucher/promo codes
|
|
||||||
- PayPal integration
|
|
||||||
- Order history
|
|
||||||
|
|
||||||
### Community
|
## Nitro V3 Update (Linux-only)
|
||||||
- Articles with comments, reactions & tags
|
|
||||||
- Photo gallery
|
|
||||||
- Leaderboard
|
|
||||||
- Teams & team applications
|
|
||||||
- Staff page & staff applications
|
|
||||||
- Rare values tracker
|
|
||||||
- Badge draw/lottery system
|
|
||||||
|
|
||||||
### Hotel Client
|
> ⚠️ **CLI only.** The web UI button has been removed. The script is configured via `.env` variables.
|
||||||
- Nitro (HTML5) client support
|
|
||||||
- Flash client support
|
|
||||||
- FindRetros integration
|
|
||||||
- VPN/proxy checker
|
|
||||||
|
|
||||||
### User System
|
**What it does:** `git pull` emulator → DB backup → SQL imports → Maven build → `git pull` Nitro_Render_V3 + Nitro-V3 → `yarn build` → sync Gamedata → cleanup → restart emulator.
|
||||||
- Public profiles with guestbook
|
|
||||||
- Two-Factor Authentication (2FA)
|
|
||||||
- Referral system with rewards
|
|
||||||
- Account & password settings
|
|
||||||
- Session logs
|
|
||||||
|
|
||||||
### Help Center
|
**Usage:**
|
||||||
- Support ticket system with replies & status management
|
|
||||||
- Website rules
|
|
||||||
- FAQ with categories
|
|
||||||
|
|
||||||
### Themes
|
```bash
|
||||||
- **Atom** — Default light theme
|
# Make sure .env contains all NITRO_* variables (see .env.example.linux)
|
||||||
- **Dusk** — Dark theme
|
cd /var/www/atomcms
|
||||||
|
bash update-Nitrov3.sh
|
||||||
|
```
|
||||||
|
|
||||||
### Filament Admin Panel
|
**Configurable via `.env`:**
|
||||||
- User & ban management
|
|
||||||
- Radio management (applications, schedules, ranks, shouts, history, banners)
|
|
||||||
- Shop & order management with charts
|
|
||||||
- Article & tag management
|
|
||||||
- Emulator settings, texts & catalog editors
|
|
||||||
- Chatlog viewer (rooms & private messages)
|
|
||||||
- Word filters & moderation tools
|
|
||||||
- Housekeeping permissions
|
|
||||||
- Website navigation & settings
|
|
||||||
- Camera/photo management
|
|
||||||
|
|
||||||
### API Endpoints
|
| Variable | Default | Description |
|
||||||
- `GET /api/user/{username}` — Fetch user data
|
|----------|---------|-------------|
|
||||||
- `GET /api/online-users` — Online users list
|
| `NITRO_EMULATOR_PATH` | `/var/www/emulator` | Emulator root directory |
|
||||||
- `GET /api/online-count` — Online user count
|
| `NITRO_EMULATOR_SERVICE` | `emulator` | Systemd service name |
|
||||||
- `GET /api/radio/current-dj` — Current DJ info
|
| `NITRO_DB_HOST` | `127.0.0.1` | Database host |
|
||||||
- `GET /api/radio/now-playing` — Current song
|
| `NITRO_DB_PORT` | `3306` | Database port |
|
||||||
- `GET /api/radio/listeners` — Listener count
|
| `NITRO_DB_NAME` | `habbo` | Database name |
|
||||||
- `GET /api/radio/shouts` — Recent shouts
|
| `NITRO_DB_USER` | `root` | Database user |
|
||||||
- `GET /api/radio/points` — Points data
|
| `NITRO_DB_PASS` | — | Database password |
|
||||||
- `GET /api/radio/points/leaderboard` — Points leaderboard
|
| `NITRO_SQL_DIR` | `{emulator}/Database Updates` | SQL updates directory |
|
||||||
|
| `NITRO_BACKUP_DIR` | `{emulator}/Database Updates/backups` | Backup directory |
|
||||||
|
| `NITRO_GAMEDATA_DIR` | `/var/www/Gamedata/config` | Gamedata config directory |
|
||||||
|
| `NITRO_CLIENT_DIR` | `{nitro}/public/configuration` | Nitro client config directory |
|
||||||
|
| `NITRO_CLIENT_SRC` | `/var/www/Nitro-V3` | Nitro-V3 source directory |
|
||||||
|
| `NITRO_RENDERER_SRC` | `/var/www/Nitro_Render_V3` | Nitro Render V3 source directory |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| Component | Requirement |
|
| Component | Version |
|
||||||
| ------------- | ------------------------------- |
|
|-----------|---------|
|
||||||
| **PHP** | 8.1 or higher |
|
| **PHP** | 8.5+ |
|
||||||
| **Database** | MariaDB 10.6+ or MySQL 8.0+ |
|
| **Database** | MariaDB 10.6+ or MySQL 8.0+ |
|
||||||
| **Web Server**| Apache (mod_rewrite) or Nginx |
|
| **Web Server** | Nginx or Apache |
|
||||||
| **Node.js** | 20 or higher |
|
| **Node.js** | 20+ |
|
||||||
| **Yarn** | 1.22+ or 4.x (Berry) |
|
| **Yarn** | 1.22+ |
|
||||||
| **Composer** | 2.x |
|
| **Composer** | 2.x |
|
||||||
|
| **Redis** | Recommended (Linux) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Environment Files
|
||||||
|
|
||||||
### Linux (Ubuntu / Debian)
|
| File | Use | Cache | DB |
|
||||||
|
|------|-----|-------|----|
|
||||||
|
| `docs/INSTALL.md` | Step-by-step setup guide | — | — |
|
||||||
|
| `.env.example.linux` | Linux production | Redis | MariaDB |
|
||||||
|
| `.env.example.windows` | Windows development | File | MySQL |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example.linux .env
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **XAMPP is not supported.** Extremely unsafe for production.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation (Ubuntu 26.04)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. System dependencies
|
# 1. System dependencies
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y git curl wget unzip nginx mariadb-server \
|
sudo apt install -y git curl wget unzip nginx mariadb-server redis-server \
|
||||||
php8.3 php8.3-cli php8.3-fpm php8.3-mysql php8.3-xml \
|
php8.5 php8.5-{cli,fpm,mysql,xml,mbstring,curl,zip,bcmath,gd,sockets,intl} \
|
||||||
php8.3-mbstring php8.3-curl php8.3-zip php8.3-bcmath \
|
build-essential
|
||||||
php8.3-gd php8.3-sockets php8.3-intl
|
|
||||||
|
|
||||||
# 2. Install Composer
|
# 2. Composer
|
||||||
curl -sS https://getcomposer.org/installer | php
|
curl -sS https://getcomposer.org/installer | php
|
||||||
sudo mv composer.phar /usr/local/bin/composer
|
sudo mv composer.phar /usr/local/bin/composer
|
||||||
|
|
||||||
# 3. Install Node.js & Yarn
|
# 3. Node.js + Yarn
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
sudo apt install -y nodejs
|
sudo apt install -y nodejs
|
||||||
sudo corepack enable
|
sudo corepack enable
|
||||||
corepack install -g yarn@latest
|
corepack install -g yarn@latest
|
||||||
|
|
||||||
# 4. Clone the project
|
# 4. Secure MariaDB
|
||||||
git clone https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git /var/www/atomcms
|
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_root_password'; FLUSH PRIVILEGES;"
|
||||||
|
|
||||||
|
# 5. Clone
|
||||||
|
git clone https://git.your-server.com/remco/Atomcms-edit.git /var/www/atomcms
|
||||||
cd /var/www/atomcms
|
cd /var/www/atomcms
|
||||||
|
|
||||||
# 5. Configure environment
|
# 6. Configure
|
||||||
cp .env.example .env
|
cp .env.example.linux .env
|
||||||
|
# EDIT .env first: set DB_PASSWORD, APP_URL, SESSION_DOMAIN
|
||||||
|
nano .env
|
||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
# Edit .env with your database credentials:
|
|
||||||
# DB_DATABASE=habbo
|
|
||||||
# DB_USERNAME=root
|
|
||||||
# DB_PASSWORD=yourpassword
|
|
||||||
|
|
||||||
# 6. Install PHP dependencies
|
# 7. Create database + user
|
||||||
|
sudo mysql -e "CREATE DATABASE IF NOT EXISTS habbo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||||
|
sudo mysql -e "CREATE USER IF NOT EXISTS 'cms'@'localhost' IDENTIFIED BY 'your_db_password';"
|
||||||
|
sudo mysql -e "GRANT ALL ON habbo.* TO 'cms'@'localhost'; FLUSH PRIVILEGES;"
|
||||||
|
|
||||||
|
# 8. Install PHP & JS deps
|
||||||
composer install --no-dev --optimize-autoloader
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
# 7. Install frontend dependencies
|
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# 8. Create database
|
# 9. Migrate, seed & cache
|
||||||
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS habbo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
|
||||||
|
|
||||||
# 9. Run migrations & seeders (or use the auto-repair tool)
|
|
||||||
php artisan migrate --seed
|
php artisan migrate --seed
|
||||||
# Or use the interactive repair tool:
|
php artisan optimize
|
||||||
# php artisan atom:check --fix
|
php artisan filament:optimize
|
||||||
|
|
||||||
# 10. Build frontend assets
|
# 10. Build frontend
|
||||||
yarn build:all
|
yarn build:all
|
||||||
|
|
||||||
# 11. Set permissions
|
# 11. Permissions
|
||||||
sudo chown -R www-data:www-data storage bootstrap/cache public/build
|
sudo chown -R www-data:www-data storage bootstrap/cache public/build
|
||||||
sudo chmod -R 775 storage bootstrap/cache
|
sudo chmod -R 775 storage bootstrap/cache
|
||||||
|
|
||||||
# 12. Configure web server
|
# 12. Sudoers (for update-Nitrov3.sh — sudo chown + systemctl)
|
||||||
|
sudo tee /etc/sudoers.d/www-data << 'EOF'
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart emulator
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl status emulator
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown -R www-data\:www-data /var/www/*
|
||||||
|
EOF
|
||||||
|
sudo chmod 440 /etc/sudoers.d/www-data
|
||||||
|
|
||||||
# --- Nginx ---
|
# 13. Start services
|
||||||
sudo nano /etc/nginx/sites-available/atomcms
|
sudo systemctl enable --now redis-server
|
||||||
|
|
||||||
|
# 14. PHP tuning
|
||||||
|
sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.5/fpm/php.ini
|
||||||
|
sudo sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/8.5/fpm/php.ini
|
||||||
|
sudo sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php/8.5/fpm/php.ini
|
||||||
|
sudo sed -i 's/max_execution_time = .*/max_execution_time = 300/' /etc/php/8.5/fpm/php.ini
|
||||||
|
|
||||||
|
# 16. Restart & verify
|
||||||
|
sudo systemctl restart php8.5-fpm redis-server nginx
|
||||||
|
php artisan about # should show green "Application" line
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nginx
|
||||||
|
|
||||||
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name your-domain.com;
|
server_name your-domain.com;
|
||||||
root /var/www/atomcms/public;
|
root /var/www/atomcms/public;
|
||||||
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN";
|
|
||||||
add_header X-Content-Type-Options "nosniff";
|
|
||||||
|
|
||||||
index index.php;
|
index index.php;
|
||||||
|
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
||||||
location / {
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
}
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
location = /favicon.ico { access_log off; log_not_found off; }
|
gzip on;
|
||||||
location = /robots.txt { access_log off; log_not_found off; }
|
gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
|
||||||
|
gzip_vary on;
|
||||||
error_page 404 /index.php;
|
|
||||||
|
|
||||||
|
location / { try_files $uri $uri/ /index.php?$query_string; }
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
|
location ~ /\.(?!well-known).* { deny all; }
|
||||||
location ~ /\.(?!well-known).* {
|
location ~ /(\.env|\.git|composer\.(json|lock)) { deny all; }
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -sf /etc/nginx/sites-available/atomcms /etc/nginx/sites-enabled/
|
sudo ln -sf /etc/nginx/sites-available/atomcms /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
sudo systemctl restart php8.5-fpm redis-server
|
||||||
# --- Apache ---
|
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw --force enable
|
||||||
sudo a2enmod rewrite
|
|
||||||
sudo nano /etc/apache2/sites-available/atomcms.conf
|
|
||||||
```
|
```
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName your-domain.com
|
|
||||||
DocumentRoot /var/www/atomcms/public
|
|
||||||
|
|
||||||
<Directory /var/www/atomcms/public>
|
### SSL (recommended)
|
||||||
Options Indexes FollowSymLinks
|
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
ErrorLog ${APACHE_LOG_DIR}/atomcms-error.log
|
|
||||||
CustomLog ${APACHE_LOG_DIR}/atomcms-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
```
|
|
||||||
```bash
|
```bash
|
||||||
sudo a2ensite atomcms
|
sudo apt install -y certbot python3-certbot-nginx
|
||||||
sudo systemctl reload apache2
|
sudo certbot --nginx -d your-domain.com
|
||||||
```
|
|
||||||
|
|
||||||
# 13. Restart PHP-FPM to clear opcache
|
|
||||||
sudo systemctl restart php8.3-fpm
|
|
||||||
|
|
||||||
# 14. Visit http://your-domain.com in your browser
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Windows (XAMPP / WampServer)
|
## Yarn Scripts
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
- [XAMPP](https://www.apachefriends.org/) (PHP 8.1+, Apache, MariaDB) or [WampServer](https://www.wampserver.com/)
|
|
||||||
- [Node.js](https://nodejs.org/) (20 LTS or higher)
|
|
||||||
- [Yarn](https://yarnpkg.com/getting-started/install) (`npm install -g yarn` after Node.js)
|
|
||||||
- [Composer](https://getcomposer.org/download/) (Windows installer)
|
|
||||||
- [Git for Windows](https://git-scm.com/download/win)
|
|
||||||
|
|
||||||
#### Steps
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 1. Open PowerShell or CMD as Administrator
|
|
||||||
|
|
||||||
# 2. Clone the project
|
|
||||||
git clone https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git C:\xampp\htdocs\atomcms
|
|
||||||
cd C:\xampp\htdocs\atomcms
|
|
||||||
|
|
||||||
# 3. Configure environment
|
|
||||||
copy .env.example .env
|
|
||||||
php artisan key:generate
|
|
||||||
|
|
||||||
# Edit .env with your XAMPP database credentials:
|
|
||||||
# DB_CONNECTION=mariadb
|
|
||||||
# DB_HOST=localhost
|
|
||||||
# DB_PORT=3306
|
|
||||||
# DB_DATABASE=habbo
|
|
||||||
# DB_USERNAME=root
|
|
||||||
# DB_PASSWORD=
|
|
||||||
|
|
||||||
# 4. Start XAMPP services
|
|
||||||
# Open XAMPP Control Panel → Start Apache & MySQL
|
|
||||||
|
|
||||||
# 5. Create database
|
|
||||||
# Open phpMyAdmin (http://localhost/phpmyadmin)
|
|
||||||
# → New → Database name: habbo → Charset: utf8mb4_unicode_ci → Create
|
|
||||||
|
|
||||||
# 6. Install PHP dependencies
|
|
||||||
composer install --no-dev --optimize-autoloader
|
|
||||||
|
|
||||||
# 7. Install frontend dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# 8. Run migrations & seeders
|
|
||||||
php artisan migrate --seed
|
|
||||||
# Or use the interactive repair tool:
|
|
||||||
# php artisan atom:check --fix
|
|
||||||
|
|
||||||
# 9. Build frontend assets
|
|
||||||
yarn build:all
|
|
||||||
|
|
||||||
# 10. Set permissions (required for storage & cache)
|
|
||||||
# Right-click storage\ and bootstrap\cache\ folders
|
|
||||||
# → Properties → Security → Edit → Add "Everyone" → Full Control
|
|
||||||
# Or run in PowerShell as Admin:
|
|
||||||
icacls storage /grant Everyone:F /T
|
|
||||||
icacls bootstrap/cache /grant Everyone:F /T
|
|
||||||
icacls public/build /grant Everyone:F /T
|
|
||||||
|
|
||||||
# 11. Enable Apache mod_rewrite
|
|
||||||
# Open XAMPP Control Panel → Apache → Config → httpd.conf
|
|
||||||
# Uncomment: LoadModule rewrite_module modules/mod_rewrite.so
|
|
||||||
|
|
||||||
# 12. Visit http://localhost/atomcms/public in your browser
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** If you use IIS on Windows, the repair tool supports auto-detection:
|
|
||||||
> `php artisan atom:check --fix`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start (Repair Tool)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Interactive mode (asks for confirmation before each step)
|
yarn build:all # Build all themes
|
||||||
php artisan atom:check --fix
|
yarn build:atom # Atom theme only
|
||||||
|
yarn build:dusk # Dusk theme only
|
||||||
# Auto mode (fixes everything automatically without asking)
|
yarn dev # Vite dev server
|
||||||
php artisan atom:check --auto
|
yarn lint # Lint JS/Vue
|
||||||
|
yarn format # Format code
|
||||||
# Force platform detection
|
|
||||||
php artisan atom:check --platform=nginx
|
|
||||||
|
|
||||||
# Dutch language output
|
|
||||||
php artisan atom:check --fix --lang=nl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check only (no changes)
|
|
||||||
php artisan atom:check
|
|
||||||
|
|
||||||
# Interactive fix (asks before each step)
|
|
||||||
php artisan atom:check --fix
|
|
||||||
|
|
||||||
# Automatic fix (no questions)
|
|
||||||
php artisan atom:check --auto
|
|
||||||
|
|
||||||
# Fix gamedata symlinks for bundled assets
|
|
||||||
php artisan atom:fix-gamedata-symlinks
|
|
||||||
|
|
||||||
# Preview symlinks (dry-run)
|
|
||||||
php artisan atom:fix-gamedata-symlinks --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Yarn Scripts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build all themes
|
|
||||||
yarn build:all
|
|
||||||
|
|
||||||
# Build single theme
|
|
||||||
yarn build:atom # Atom theme
|
|
||||||
yarn build:dusk # Dusk theme
|
|
||||||
|
|
||||||
# Development
|
|
||||||
yarn dev # Start Vite dev server
|
|
||||||
yarn dev:atom # Dev server for Atom theme
|
|
||||||
|
|
||||||
# Linting & Formatting
|
|
||||||
yarn lint # Check JS/Vue
|
|
||||||
yarn lint:fix # Fix JS/Vue
|
|
||||||
yarn lint:css # Check CSS
|
|
||||||
yarn lint:css:fix # Fix CSS
|
|
||||||
yarn format # Format everything
|
|
||||||
yarn format:check # Check formatting
|
|
||||||
|
|
||||||
# Full check
|
|
||||||
yarn check # Lint + format check
|
|
||||||
yarn check:php # PHP syntax check
|
|
||||||
yarn check:security # Composer & npm audit
|
|
||||||
yarn check:deps # Check outdated packages
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
yarn clean # Clear Vite cache
|
|
||||||
yarn rebuild # Clean + install + build
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Auto-fix code style
|
|
||||||
php artisan atom:fix-code
|
|
||||||
|
|
||||||
# Static analysis (PHPStan Level 3)
|
|
||||||
./vendor/bin/phpstan analyse
|
|
||||||
|
|
||||||
# Build frontend assets
|
|
||||||
yarn build:atom
|
|
||||||
yarn build:dusk
|
|
||||||
|
|
||||||
# Development mode
|
|
||||||
yarn dev:atom
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Repair Tool Details
|
|
||||||
|
|
||||||
The `atom:check` command performs over **100 deep system checks** and offers to fix them automatically.
|
|
||||||
|
|
||||||
### Diagnostic Checks
|
|
||||||
|
|
||||||
| Section | What it Checks |
|
|
||||||
| ------------- | ------------------------------------------------------------- |
|
|
||||||
| **Environment** | .env, APP_KEY, Debug mode, Composer security |
|
|
||||||
| **Database** | Tables, columns, migrations, seeders, settings, radio, admin |
|
|
||||||
| **PHP Stack** | Extensions, php.ini, config cache, session |
|
|
||||||
| **Web Server**| Apache/Nginx/IIS config, SSL |
|
|
||||||
| **System** | Permissions, firewall ports |
|
|
||||||
| **Assets** | Frontend, Redis, Cron, Queue, Supervisor, Vite |
|
|
||||||
| **HTTP Errors** | 400, 401, 403, 404, 419, 429, 500, 502, 503, 504 |
|
|
||||||
|
|
||||||
### Auto-Fix Steps
|
|
||||||
|
|
||||||
| Step | Action |
|
|
||||||
| ---- | ---------------------------------- |
|
|
||||||
| 1 | Environment (.env, APP_KEY) |
|
|
||||||
| 2 | Clear all caches |
|
|
||||||
| 3 | Fix permissions |
|
|
||||||
| 4 | Run migrations |
|
|
||||||
| 5 | Run seeders |
|
|
||||||
| 6 | Fix storage (symlink, directories) |
|
|
||||||
| 6b | Fix Radio tables |
|
|
||||||
| 7 | Fix Gamedata symlinks |
|
|
||||||
| 8 | Create admin user |
|
|
||||||
| 9 | Web server config (.htaccess) |
|
|
||||||
| 10 | PHP config & extensions |
|
|
||||||
| 11 | Build assets (npm) |
|
|
||||||
| 12 | Fix HTTP errors |
|
|
||||||
|
|
||||||
Auto-detects: **Linux, Windows IIS, XAMPP, WAMP, Apache, Nginx**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
| Optimization | Description |
|
|
||||||
| ------------------- | -------------------------------- |
|
|
||||||
| **Vite 8** | Fastest build tool |
|
|
||||||
| **esbuild** | Faster minification |
|
|
||||||
| **Better chunking** | Optimal code splitting |
|
|
||||||
| **Gzip + Brotli** | Compression (~70% smaller) |
|
|
||||||
| **Resource hints** | DNS prefetch, preconnect, preload|
|
|
||||||
| **HTTP/2** | Faster network requests |
|
|
||||||
| **Console removed** | Smaller JS bundles |
|
|
||||||
| **Caching** | Vite cache + optimizedeps |
|
|
||||||
|
|
||||||
### Gamedata Symlinks
|
|
||||||
|
|
||||||
The `atom:fix-gamedata-symlinks` command creates symlinks in the Gamedata directory to point to optimized bundled assets, significantly improving game client load times.
|
|
||||||
|
|
||||||
| Symlink | Target | Purpose |
|
|
||||||
| ---------------- | ------------------- | ---------------- |
|
|
||||||
| `effect` | `bundled/effect` | Avatar effects |
|
|
||||||
| `furniture` | `bundled/furniture` | Furniture assets |
|
|
||||||
| `generic` | `bundled/generic` | Generic assets |
|
|
||||||
| `pet` | `bundled/pet` | Pet assets |
|
|
||||||
| `figure` | `bundled/figure` | Avatar figures |
|
|
||||||
| `generic_custom` | `bundled/generic` | Custom generic |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Backend:** Laravel 13
|
**Laravel 13 · React 19 + Alpine.js · Vite 8 · TailwindCSS 4 · Filament 5 · MariaDB/MySQL · Redis**
|
||||||
- **Frontend:** React 19 + Alpine.js
|
|
||||||
- **Build:** Vite 8
|
---
|
||||||
- **CSS:** TailwindCSS 4
|
|
||||||
- **Admin Panel:** Filament 5
|
## Security
|
||||||
- **Database:** MariaDB / MySQL
|
|
||||||
- **Linting:** ESLint + Stylelint + Prettier
|
AtomCMS is built with security as a priority. Below is what's in place and what you need to configure.
|
||||||
|
|
||||||
|
### ✅ Already locked down
|
||||||
|
|
||||||
|
| Measure | Details |
|
||||||
|
|---------|---------|
|
||||||
|
| **Mass assignment protection** | User model restricted to 21 fillable fields (sensitive fields like `rank`, `credits`, `online` require explicit `forceFill`) |
|
||||||
|
| **API authentication** | Sanctum tokens, Bearer-only (no query-string API keys accepted) |
|
||||||
|
| **PayPal credentials** | Loaded from `env()`, never hardcoded |
|
||||||
|
| **CORS** | Must be explicitly set via `CORS_ALLOWED_ORIGINS` env (no wildcard default) |
|
||||||
|
| **Debug mode** | `APP_DEBUG=false` by default |
|
||||||
|
| **PHP debugging** | No `dd()`, `dump()`, or `var_dump()` in production code |
|
||||||
|
| **Password flashing** | Exception handler excludes passwords from session flash |
|
||||||
|
| **File uploads** | MIME validation (Laravel `image` rule + `finfo` on logos) |
|
||||||
|
| **2FA** | Two-factor authentication available |
|
||||||
|
| **SQL injection** | All queries use parameterized binding or Eloquent ORM |
|
||||||
|
| **Command injection** | All `exec()`/`shell_exec()` calls use `escapeshellarg()` or hardcoded values |
|
||||||
|
| **CSRF** | Sanctum CSRF protection on all stateful routes |
|
||||||
|
| **Insecure deserialization** | No `unserialize()` calls exist |
|
||||||
|
|
||||||
|
### ⚠️ You must configure
|
||||||
|
|
||||||
|
| Item | What to do |
|
||||||
|
|------|------------|
|
||||||
|
| **`.env` file** | Restrict file permissions (`chmod 600 .env`), ensure Nginx blocks access (already in the provided config) |
|
||||||
|
| **`CORS_ALLOWED_ORIGINS`** | Set to your exact frontend domain(s) in `.env` (included in the example files) |
|
||||||
|
| **Database password** | Use a strong, unique password (not `your_db_password`) |
|
||||||
|
| **APP_KEY** | Run `php artisan key:generate` after cloning |
|
||||||
|
| **Session domain** | Set `SESSION_DOMAIN` to your hotel domain in `.env` |
|
||||||
|
| **SSL** | Required for production — use the Certbot instructions above |
|
||||||
|
| **Admin accounts** | Only grant high-rank access to trusted users |
|
||||||
|
| **Log retention** | Check `LOG_MAX_FILES` in `.env` (default 14 days) |
|
||||||
|
|
||||||
|
### 🔒 Sudoers safety
|
||||||
|
|
||||||
|
The `sudoers.d/www-data` configuration grants passwordless `systemctl` and `chown` to `www-data`. This is **safe by design**:
|
||||||
|
|
||||||
|
- Each command is pinned to a specific binary path (`/usr/bin/systemctl`, `/usr/bin/chown`)
|
||||||
|
- `chown` is restricted to `/var/www/*`
|
||||||
|
- No shell (`/bin/sh`, `/bin/bash`) is granted
|
||||||
|
- No arbitrary binaries can be executed
|
||||||
|
- In a worst-case web compromise, the attacker still cannot read `/etc/shadow`, install packages, or run arbitrary commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Discord:** [Join our server](https://discord.gg/pP6HyZedAj)
|
||||||
|
- **Issues:** Report bugs via the project issue tracker
|
||||||
|
- **Contributions:** Fork & submit merge requests — all help is welcome!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- **Remco (Epicnabbo)** — Core Maintainer, System Architecture
|
**Remco (Epicnabbo)** — Core Maintainer · **Kasja** — Design & Themes · **Kani** — RCON & API · **Atom Community** — Testing & Feedback
|
||||||
- **Kasja** — Design & Themes
|
|
||||||
- **Kani** — RCON & API
|
|
||||||
- **Atom Community** — Testing & Feedback
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center"><i>Made with love for the Retro Community</i></div>
|
||||||
<i>Made with love for the Retro Community</i>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
# AtomCMS (epicnabbo edit) - v1.0 Ultimate Setup Guide
|
|
||||||
|
|
||||||
**AtomCMS** is a powerful Habbo retro CMS. This guide ensures a **100% working installation** on **any system**: Linux (Nginx/Apache), Windows (IIS/XAMPP), and macOS.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone ssh://git@localhost:8422/RemcoEpic/atomcms-edit.git
|
|
||||||
cd atomcms-edit
|
|
||||||
composer install --no-dev
|
|
||||||
cp .env.example .env
|
|
||||||
php artisan key:generate
|
|
||||||
# Configure your .env (DB settings below)
|
|
||||||
php artisan migrate --seed
|
|
||||||
php artisan storage:link
|
|
||||||
php artisan serve
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 System Requirements
|
|
||||||
|
|
||||||
### Universal Requirements
|
|
||||||
|
|
||||||
- **PHP**: 8.1, 8.2, or 8.3 (8.2 Recommended)
|
|
||||||
- **Extensions**: `pdo_mysql`, `curl`, `mbstring`, `openssl`, `tokenizer`, `xml`, `zip`
|
|
||||||
- **Database**: MySQL 5.7+ or MariaDB 10.3+
|
|
||||||
- **Composer**: 2.x
|
|
||||||
|
|
||||||
### Linux (Ubuntu/Debian)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y php8.2 php8.2-{mysql,curl,mbstring,xml,zip,fpm} nginx mysql-server
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows (XAMPP/IIS)
|
|
||||||
|
|
||||||
- Download **XAMPP** with PHP 8.2 or install **PHP 8.2** manually.
|
|
||||||
- Ensure `php.ini` has `extension=pdo_mysql`, `extension=curl`, etc. enabled.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Step 1: Clone the Repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For RemcoEpic epicnabbo edit version:
|
|
||||||
git clone ssh://git@localhost:8422/RemcoEpic/atomcms-edit.git
|
|
||||||
cd atomcms-edit
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Step 2: Install Dependencies
|
|
||||||
|
|
||||||
Run composer to install all required packages. The system is optimized to handle missing extensions automatically via the `SystemCheck` middleware.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install --optimize-autoloader
|
|
||||||
```
|
|
||||||
|
|
||||||
_Windows Note: Run CMD/PowerShell as Administrator if you get permission errors._
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Step 3: Environment Configuration
|
|
||||||
|
|
||||||
### 3.1 Create .env
|
|
||||||
|
|
||||||
If `.env` is missing, the system tries to create it automatically. You can also do it manually:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Generate App Key
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan key:generate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Database Settings
|
|
||||||
|
|
||||||
Edit `.env` and configure your database connection:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=atomcms
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
_Windows IIS Note: If `127.0.0.1` fails, try `localhost`._
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💾 Step 4: Database Setup
|
|
||||||
|
|
||||||
### 4.1 Create Database
|
|
||||||
|
|
||||||
Login to MySQL and create the database:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE atomcms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
CREATE USER 'atomcms_user'@'localhost' IDENTIFIED BY 'strong_password';
|
|
||||||
GRANT ALL PRIVILEGES ON atomcms.* TO 'atomcms_user'@'localhost';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
EXIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Run Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan migrate --seed
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔗 Step 5: Storage Link (Crucial!)
|
|
||||||
|
|
||||||
The system uses a **Bulletproof Middleware (`SystemCheck`)** that attempts to create the storage symlink automatically on the first request.
|
|
||||||
|
|
||||||
**Manual Fallback (if auto-fails):**
|
|
||||||
|
|
||||||
_Linux:_
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan storage:link
|
|
||||||
sudo chmod -R 755 storage bootstrap/cache
|
|
||||||
```
|
|
||||||
|
|
||||||
_Windows (CMD as Admin):_
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
mklink /J "C:\path\to\atomcms-edit\public\storage" "C:\path\to\atomcms-edit\storage\app\public"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Step 6: Web Server Configuration
|
|
||||||
|
|
||||||
### Option A: PHP Built-in (Testing)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan serve --host=0.0.0.0 --port=8000
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit: `http://localhost:8000`
|
|
||||||
|
|
||||||
### Option B: Nginx (Linux Production)
|
|
||||||
|
|
||||||
Edit `/etc/nginx/sites-available/atomcms`:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name epicnabbo.nl; # Your domain
|
|
||||||
root /var/www/atomcms/public;
|
|
||||||
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN";
|
|
||||||
add_header X-Content-Type-Options "nosniff";
|
|
||||||
|
|
||||||
index index.php;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
|
||||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
|
||||||
include fastcgi_params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Enable: `sudo ln -s /etc/nginx/sites-available/atomcms /etc/nginx/sites-enabled/ && sudo systemctl restart nginx`
|
|
||||||
|
|
||||||
### Option C: Windows IIS
|
|
||||||
|
|
||||||
1. Open **IIS Manager** -> **Sites** -> **Add Website**.
|
|
||||||
2. Physical Path: `C:\inetpub\wwwroot\atomcms-edit\public`
|
|
||||||
3. Install **URL Rewrite Module**.
|
|
||||||
4. Add `web.config` in `public/`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<configuration>
|
|
||||||
<system.webServer>
|
|
||||||
<rewrite>
|
|
||||||
<rules>
|
|
||||||
<rule name="Laravel Routes" stopProcessing="true">
|
|
||||||
<match url=".*" />
|
|
||||||
<conditions>
|
|
||||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
|
||||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
|
||||||
</conditions>
|
|
||||||
<action type="Rewrite" url="index.php" />
|
|
||||||
</rule>
|
|
||||||
</rules>
|
|
||||||
</rewrite>
|
|
||||||
</system.webServer>
|
|
||||||
</configuration>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛡️ Step 7: Production Optimization
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php artisan config:cache
|
|
||||||
php artisan route:cache
|
|
||||||
php artisan view:cache
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Step 8: Verify & Test
|
|
||||||
|
|
||||||
1. Open `http://your-domain.com` (or `http://localhost:8000`).
|
|
||||||
2. Test **Registration** at `/register`.
|
|
||||||
- _Note:_ v1.0 fixes the "500 Error" on registration (Cloudflare Turnstile & IP fields).
|
|
||||||
3. Check logs if needed: `tail -f storage/logs/laravel.log`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Troubleshooting (v1.0 Fixes)
|
|
||||||
|
|
||||||
### 500 Error on Registration?
|
|
||||||
|
|
||||||
**Fixed in v1.0!**
|
|
||||||
|
|
||||||
- ✅ Cloudflare Turnstile null-response handled.
|
|
||||||
- ✅ `ip_register` and `ip_current` added to User model.
|
|
||||||
- ✅ MySQL Strict Mode disabled for Windows/IIS/XAMPP compatibility.
|
|
||||||
|
|
||||||
### "Missing .env" or "No Application Key"
|
|
||||||
|
|
||||||
The `SystemCheck` middleware (auto-runs on every request) will attempt to:
|
|
||||||
|
|
||||||
1. Copy `.env.example` to `.env`.
|
|
||||||
2. Generate `APP_KEY` automatically.
|
|
||||||
|
|
||||||
### Storage Link Issues
|
|
||||||
|
|
||||||
The system auto-creates the symlink. If it fails on Windows, ensure you run the server with **Administrator** privileges.
|
|
||||||
|
|
||||||
### Database Connection Failed
|
|
||||||
|
|
||||||
1. Check `.env` credentials.
|
|
||||||
2. Ensure MySQL service is running (`systemctl status mysql` or Windows Services).
|
|
||||||
3. Use `127.0.0.1` instead of `localhost` if socket errors occur.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 What's new in v1.0 (epicnabbo edit)?
|
|
||||||
|
|
||||||
This version is **Bulletproof**. It is designed to work immediately after `composer install` on any environment.
|
|
||||||
|
|
||||||
- ✅ **Cross-Platform**: Works on Linux (Nginx/Apache), Windows (IIS/XAMPP).
|
|
||||||
- ✅ **Auto-Recovery**: `SystemCheck` middleware fixes missing `.env`, `APP_KEY`, and storage links automatically.
|
|
||||||
- ✅ **Zero 500 Errors**: Registration bugs (Turnstile & IP fields) fixed.
|
|
||||||
- ✅ **MySQL Strict Mode**: Disabled for maximum compatibility.
|
|
||||||
- ✅ **Windows Path Fix**: `index.php` handles Windows backslash paths.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**© 2026 RemcoEpic - epicnabbo.nl**
|
|
||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Actions\Commandocentrum;
|
namespace App\Actions\Commandocentrum;
|
||||||
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use App\Services\RconService;
|
use App\Services\RconService;
|
||||||
use App\Services\SettingsService;
|
use App\Services\SettingsService;
|
||||||
use Illuminate\Support\Facades\Process;
|
use Illuminate\Support\Facades\Process;
|
||||||
@@ -13,7 +12,6 @@ class EmulatorControlAction
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SettingsService $settings,
|
private readonly SettingsService $settings,
|
||||||
private readonly EmulatorUpdateService $updateService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function start(): array
|
public function start(): array
|
||||||
@@ -59,29 +57,4 @@ class EmulatorControlAction
|
|||||||
|
|
||||||
return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!'];
|
return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build(): array
|
|
||||||
{
|
|
||||||
return $this->updateService->buildFromSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(): array
|
|
||||||
{
|
|
||||||
return $this->updateService->updateEmulator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function runSqlUpdates(): array
|
|
||||||
{
|
|
||||||
return $this->updateService->runSqlUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBackups(): array
|
|
||||||
{
|
|
||||||
return $this->updateService->getBackupList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function restoreBackup(string $backupName): array
|
|
||||||
{
|
|
||||||
return $this->updateService->restoreBackup($backupName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Actions\Commandocentrum;
|
|
||||||
|
|
||||||
use App\Services\SettingsService;
|
|
||||||
use Illuminate\Support\Facades\Artisan;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class NitroControlAction
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly SettingsService $settings,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function pullUpdates(string $clientPath, string $rendererPath, string $branch): array
|
|
||||||
{
|
|
||||||
$this->runGitPull($clientPath, $branch);
|
|
||||||
$this->runGitPull($rendererPath, $branch);
|
|
||||||
|
|
||||||
return ['success' => true, 'message' => 'Nitro bijgewerkt van GitHub'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function build(string $clientPath, string $rendererPath, string $branch): array
|
|
||||||
{
|
|
||||||
$this->runGitPull($clientPath, $branch);
|
|
||||||
$this->runGitPull($rendererPath, $branch);
|
|
||||||
|
|
||||||
Process::timeout(120)->run('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1');
|
|
||||||
Process::timeout(120)->run('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1');
|
|
||||||
|
|
||||||
$exitCode = Artisan::call('build:theme');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => $exitCode === 0,
|
|
||||||
'message' => $exitCode === 0 ? 'Nitro build succesvol!' : 'Build gestart - controleer handmatig',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateConfigs(string $siteUrl, string $webroot, string $gamedataPath): array
|
|
||||||
{
|
|
||||||
if (! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
|
|
||||||
return ['success' => false, 'message' => 'Voer een geldige URL in'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingConfigs = $this->readExistingConfigs($webroot, $gamedataPath);
|
|
||||||
|
|
||||||
$exitCode = Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]);
|
|
||||||
|
|
||||||
if ($existingConfigs !== [] && $exitCode === 0) {
|
|
||||||
$this->mergeExistingConfigs($webroot, $existingConfigs);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->settings->set('nitro_last_checked', now()->toIso8601String());
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => $exitCode === 0,
|
|
||||||
'message' => $exitCode === 0 ? 'Configs gegenereerd & bestaande instellingen behouden!' : 'Config gegenereerd (controleer handmatig)',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runGitPull(string $path, string $branch): void
|
|
||||||
{
|
|
||||||
Process::timeout(60)->run('cd ' . escapeshellarg($path) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function readExistingConfigs(string $webroot, string $gamedataPath): array
|
|
||||||
{
|
|
||||||
$configs = [];
|
|
||||||
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$path = $webroot . '/' . $file;
|
|
||||||
$content = @file_get_contents($path);
|
|
||||||
if ($content) {
|
|
||||||
$decoded = json_decode($content, true);
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE) {
|
|
||||||
$configs[$file] = $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gamedataPath !== '') {
|
|
||||||
$gamedataConfigs = [
|
|
||||||
'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json',
|
|
||||||
'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json',
|
|
||||||
'ProductData.json' => $gamedataPath . '/config/ProductData.json',
|
|
||||||
'FigureData.json' => $gamedataPath . '/config/FigureData.json',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($gamedataConfigs as $key => $path) {
|
|
||||||
$content = @file_get_contents($path);
|
|
||||||
if ($content) {
|
|
||||||
$decoded = json_decode($content, true);
|
|
||||||
if (json_last_error() === JSON_ERROR_NONE) {
|
|
||||||
$configs['gamedata.' . $key] = $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mergeExistingConfigs(string $webroot, array $existingConfigs): void
|
|
||||||
{
|
|
||||||
if (isset($existingConfigs['renderer-config.json'])) {
|
|
||||||
$newPath = $webroot . '/renderer-config.json';
|
|
||||||
$newContent = @file_get_contents($newPath);
|
|
||||||
$newConfig = json_decode($newContent, true);
|
|
||||||
|
|
||||||
if ($newConfig && json_last_error() === JSON_ERROR_NONE) {
|
|
||||||
$merged = array_merge($existingConfigs['renderer-config.json'], $newConfig);
|
|
||||||
@file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -171,13 +171,11 @@ class CreateNewUser implements CreatesNewUsers
|
|||||||
try {
|
try {
|
||||||
Http::asJson()->post(is_string($discordWebhookUrl) ? $discordWebhookUrl : '', [
|
Http::asJson()->post(is_string($discordWebhookUrl) ? $discordWebhookUrl : '', [
|
||||||
'username' => sprintf('%s Bot', is_string($hotelNameSetting) ? $hotelNameSetting : 'Hotel'),
|
'username' => sprintf('%s Bot', is_string($hotelNameSetting) ? $hotelNameSetting : 'Hotel'),
|
||||||
'content' => "User: {$username} has just registered, with the IP: {$ip} and E-mail: {$email}",
|
'content' => "User: {$username} has just registered.",
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Failed to send Discord webhook notification', [
|
Log::error('Failed to send Discord webhook notification', [
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'ip' => $ip,
|
|
||||||
'email' => $email,
|
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class DisableTwoFactorAuthentication extends \Laravel\Fortify\Actions\DisableTwo
|
|||||||
$user->forceFill([
|
$user->forceFill([
|
||||||
'two_factor_secret' => null,
|
'two_factor_secret' => null,
|
||||||
'two_factor_recovery_codes' => null,
|
'two_factor_recovery_codes' => null,
|
||||||
'two_factor_confirmed' => false,
|
'two_factor_confirmed_at' => null,
|
||||||
])->save();
|
])->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Enums\AlertChannel;
|
|
||||||
use App\Enums\AlertType;
|
|
||||||
use App\Services\AlertService;
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class AutoUpdateCommand extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'update:auto
|
|
||||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
|
||||||
{--sql-only : Alleen SQL updates draaien}
|
|
||||||
{--emu-only : Alleen emulator updates draaien}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Automatische emulator en SQL updates uitvoeren';
|
|
||||||
|
|
||||||
private const string CACHE_KEY_LAST_EMU_UPDATE = 'auto_update_last_emu';
|
|
||||||
|
|
||||||
private const string CACHE_KEY_LAST_SQL_UPDATE = 'auto_update_last_sql';
|
|
||||||
|
|
||||||
public function handle(AlertService $alertService, EmulatorUpdateService $updateService): int
|
|
||||||
{
|
|
||||||
$this->info('Automatische update check...');
|
|
||||||
|
|
||||||
$isForced = $this->option('force');
|
|
||||||
$sqlOnly = $this->option('sql-only');
|
|
||||||
$emuOnly = $this->option('emu-only');
|
|
||||||
|
|
||||||
if (! $isForced && ! $this->isScheduledTime()) {
|
|
||||||
$this->line('Niet de geplande tijd, overslaan.');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $sqlOnly) {
|
|
||||||
$this->runEmulatorUpdate($alertService, $updateService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $emuOnly) {
|
|
||||||
$this->runSqlUpdates($alertService, $updateService);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isScheduledTime(): bool
|
|
||||||
{
|
|
||||||
$enabled = setting('auto_update_enabled', true);
|
|
||||||
|
|
||||||
if (! $enabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scheduleTime = setting('auto_update_schedule', '03:00');
|
|
||||||
$scheduleDays = setting('auto_update_days', '0,1,2,3,4,5,6');
|
|
||||||
|
|
||||||
$now = now();
|
|
||||||
$currentTime = $now->format('H:i');
|
|
||||||
$currentDay = (int) $now->dayOfWeek;
|
|
||||||
|
|
||||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
|
||||||
|
|
||||||
if (! in_array($currentDay, $allowedDays)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($currentTime !== $scheduleTime) {
|
|
||||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
|
||||||
if ($minuteDiff > 5) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runEmulatorUpdate(AlertService $alertService, EmulatorUpdateService $updateService): void
|
|
||||||
{
|
|
||||||
$check = $updateService->checkForUpdates();
|
|
||||||
|
|
||||||
if (! ($check['update_available'] ?? false)) {
|
|
||||||
$this->line('Emulator is al up-to-date.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->warn('Nieuwe emulator versie beschikbaar: ' . ($check['latest_version'] ?? 'onbekend'));
|
|
||||||
$this->info('Emulator updaten...');
|
|
||||||
|
|
||||||
$result = $updateService->updateEmulator();
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$this->info('Emulator succesvol geüpdatet!');
|
|
||||||
$alertService->sendEmulatorUpdate($result['version'] ?? 'onbekend', $result['message'] ?? '');
|
|
||||||
Log::info('[AutoUpdate] Emulator updated to v' . ($result['version'] ?? 'unknown'));
|
|
||||||
} else {
|
|
||||||
$this->error('Emulator update mislukt: ' . ($result['error'] ?? 'onbekende fout'));
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ERROR,
|
|
||||||
'Emulator Auto-Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::put(self::CACHE_KEY_LAST_EMU_UPDATE, now()->toIso8601String());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runSqlUpdates(AlertService $alertService, EmulatorUpdateService $updateService): void
|
|
||||||
{
|
|
||||||
$check = $updateService->checkForSqlUpdates();
|
|
||||||
|
|
||||||
if (! ($check['has_updates'] ?? false)) {
|
|
||||||
$this->line('Geen SQL updates beschikbaar.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->warn($check['message']);
|
|
||||||
$this->info('SQL updates uitvoeren...');
|
|
||||||
|
|
||||||
$result = $updateService->runSqlUpdates();
|
|
||||||
|
|
||||||
if ($result['success'] && ! empty($result['files_run'])) {
|
|
||||||
$count = count($result['files_run']);
|
|
||||||
$this->info("{$count} SQL updates succesvol uitgevoerd!");
|
|
||||||
$alertService->sendSqlUpdate($count, $result['message'] ?? '');
|
|
||||||
Log::info('[AutoUpdate] SQL updates completed', ['files' => $result['files_run']]);
|
|
||||||
} elseif (! empty($result['errors'])) {
|
|
||||||
$this->warn('SQL updates met fouten: ' . implode(', ', $result['errors']));
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::CRITICAL_ERROR,
|
|
||||||
'SQL Updates Met Fouten: ' . implode(', ', $result['errors']),
|
|
||||||
['files' => $result['errors']],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::put(self::CACHE_KEY_LAST_SQL_UPDATE, now()->toIso8601String());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Enums\AlertChannel;
|
|
||||||
use App\Enums\AlertType;
|
|
||||||
use App\Services\AlertService;
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class EmulatorUpdateCommand extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'emulator:update
|
|
||||||
{--check : Alleen controleren op updates}
|
|
||||||
{--force : Forceer update ook al is er geen nieuwe versie}
|
|
||||||
{--repair : Probeer emulator te repareren}
|
|
||||||
{--rebuild : Forceer build vanaf source}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Update de emulator vanaf GitHub';
|
|
||||||
|
|
||||||
public function handle(EmulatorUpdateService $updateService, AlertService $alertService): int
|
|
||||||
{
|
|
||||||
if ($this->option('repair')) {
|
|
||||||
return $this->repairEmulator($updateService, $alertService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $updateService->isConfigured()) {
|
|
||||||
$this->error('Geen GitHub URL geconfigureerd voor emulator updates.');
|
|
||||||
$this->info('Configureer dit in Filament > Settings > Emulator');
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Emulator update service gestart...');
|
|
||||||
|
|
||||||
$checkResult = $updateService->checkForUpdates();
|
|
||||||
|
|
||||||
if (isset($checkResult['error'])) {
|
|
||||||
$this->error($checkResult['error']);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->table(
|
|
||||||
['Property', 'Value'],
|
|
||||||
[
|
|
||||||
['Huidige Versie', $checkResult['current_version']],
|
|
||||||
['Nieuwste Versie', $checkResult['latest_version']],
|
|
||||||
['Update Beschikbaar', $checkResult['update_available'] ? 'JA' : 'NEE'],
|
|
||||||
['Type', $checkResult['update_type'] ?? 'N/A'],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($this->option('check')) {
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$force = $this->option('force');
|
|
||||||
$rebuild = $this->option('rebuild');
|
|
||||||
|
|
||||||
if (! $checkResult['update_available'] && ! $force && ! $rebuild) {
|
|
||||||
$this->info('Emulator is al up-to-date!');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $checkResult['update_available'] && ($force || $rebuild)) {
|
|
||||||
$this->warn('Geen nieuwe versie beschikbaar, maar force/rebuild aangevraagd.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $force && ! $rebuild && ! $this->confirm('Wil je de emulator updaten naar v' . $checkResult['latest_version'] . '?')) {
|
|
||||||
$this->info('Update geannuleerd.');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Emulator wordt geüpdatet...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($rebuild || $checkResult['type'] === 'source_build') {
|
|
||||||
$this->info('Build vanaf source...');
|
|
||||||
$result = $updateService->buildFromSource($force);
|
|
||||||
} else {
|
|
||||||
$result = $updateService->updateEmulator();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$this->info($result['message'] ?? 'Emulator succesvol geüpdatet!');
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ONLINE,
|
|
||||||
'Emulator succesvol geüpdatet naar v' . ($result['version'] ?? 'onbekend'),
|
|
||||||
[
|
|
||||||
'version' => $result['version'] ?? 'onbekend',
|
|
||||||
'jar' => $result['jar'] ?? 'N/A',
|
|
||||||
],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
Log::info('[EmulatorUpdateCommand] Update successful', $result);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error('Update mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ERROR,
|
|
||||||
'Emulator update mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
Log::error('[EmulatorUpdateCommand] Update failed', $result);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('Update exception: ' . $e->getMessage());
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::CRITICAL_ERROR,
|
|
||||||
'Emulator update exception: ' . $e->getMessage(),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
Log::error('[EmulatorUpdateCommand] Exception', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function repairEmulator(EmulatorUpdateService $updateService, AlertService $alertService): int
|
|
||||||
{
|
|
||||||
$this->warn('🔧 Emulator repair modus gestart...');
|
|
||||||
$this->info('Dit zal de emulator status controleren en proberen te repareren.');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$repairResult = $updateService->repairEmulator();
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$this->info('✅ Repair succesvol!');
|
|
||||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ONLINE,
|
|
||||||
'Emulator gerepareerd',
|
|
||||||
['actions' => $repairResult['actions'] ?? []],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ERROR,
|
|
||||||
'Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,796 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class GenerateNitroConfigs extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'app:generate-nitro-configs
|
|
||||||
{--site-url= : The site URL to use}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Generate Nitro configuration files (renderer-config.json, ui-config.json, UITexts.json)';
|
|
||||||
|
|
||||||
private array $checks = [];
|
|
||||||
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$siteUrl = $this->option('site-url') ?? setting('nitro_site_url', config('app.url', 'https://epicnabbo.nl'));
|
|
||||||
|
|
||||||
$this->info('🔧 Nitro Config Generator');
|
|
||||||
$this->line('═══════════════════════════════════════════════');
|
|
||||||
|
|
||||||
// Check 1: Validate URL
|
|
||||||
$this->addCheck(function () use ($siteUrl) {
|
|
||||||
if (empty($siteUrl) || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
|
|
||||||
throw new \Exception("Invalid URL: {$siteUrl}");
|
|
||||||
}
|
|
||||||
$this->line(" ✓ URL: {$siteUrl}");
|
|
||||||
});
|
|
||||||
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
$status = $nitroService->getStatus();
|
|
||||||
$webroot = $status['webroot'] ?? '/var/www/Client';
|
|
||||||
$buildPath = $status['build_path'] ?? '/var/www/atomcms/nitro-client/dist';
|
|
||||||
|
|
||||||
// Check 2: Webroot exists
|
|
||||||
$this->addCheck(function () use ($webroot) {
|
|
||||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $webroot));
|
|
||||||
if ($result->exitCode() !== 0) {
|
|
||||||
throw new \Exception("Webroot does not exist: {$webroot}");
|
|
||||||
}
|
|
||||||
$this->line(" ✓ Webroot exists: {$webroot}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check 3: Webroot is writable
|
|
||||||
$this->addCheck(function () use ($webroot) {
|
|
||||||
$result = Process::timeout(5)->run('test -w ' . escapeshellarg((string) $webroot));
|
|
||||||
if ($result->exitCode() !== 0) {
|
|
||||||
throw new \Exception("Webroot is not writable: {$webroot}");
|
|
||||||
}
|
|
||||||
$this->line(' ✓ Webroot is writable');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check 4: Build path exists
|
|
||||||
$this->addCheck(function () use ($buildPath) {
|
|
||||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $buildPath));
|
|
||||||
if ($result->exitCode() !== 0) {
|
|
||||||
throw new \Exception("Build path does not exist: {$buildPath}");
|
|
||||||
}
|
|
||||||
$this->line(" ✓ Build path exists: {$buildPath}");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check both buildPath and webroot for example files
|
|
||||||
$this->addCheck(function () use ($buildPath, $webroot) {
|
|
||||||
// Map .example to .json fallback
|
|
||||||
$fileMap = [
|
|
||||||
'renderer-config.example' => 'renderer-config.json',
|
|
||||||
'ui-config.example' => 'ui-config.json',
|
|
||||||
'UITexts.example' => 'UITexts.example',
|
|
||||||
];
|
|
||||||
|
|
||||||
$allFound = true;
|
|
||||||
|
|
||||||
foreach ($fileMap as $exampleFile => $jsonFile) {
|
|
||||||
// Check .example file first
|
|
||||||
$buildPathResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $exampleFile));
|
|
||||||
$webrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $exampleFile));
|
|
||||||
|
|
||||||
if ($buildPathResult->exitCode() === 0) {
|
|
||||||
$this->line(" ✓ Found in build path: {$exampleFile}");
|
|
||||||
} elseif ($webrootResult->exitCode() === 0) {
|
|
||||||
$this->line(" ✓ Found in webroot: {$exampleFile}");
|
|
||||||
} elseif ($jsonFile !== $exampleFile) {
|
|
||||||
// Check json fallback for .example files
|
|
||||||
$jsonBuildResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $jsonFile));
|
|
||||||
$jsonWebrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $jsonFile));
|
|
||||||
if ($jsonBuildResult->exitCode() === 0) {
|
|
||||||
$this->line(" ✓ Found in build path: {$jsonFile}");
|
|
||||||
} elseif ($jsonWebrootResult->exitCode() === 0) {
|
|
||||||
$this->line(" ✓ Found in webroot: {$jsonFile}");
|
|
||||||
} else {
|
|
||||||
$this->warn(" ⚠ Missing: {$exampleFile} or {$jsonFile}");
|
|
||||||
$allFound = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->warn(" ⚠ Missing: {$exampleFile}");
|
|
||||||
$allFound = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $allFound) {
|
|
||||||
throw new \Exception('Some example files are missing');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check 6: Check disk space
|
|
||||||
$this->addCheck(function () use ($webroot) {
|
|
||||||
$result = Process::timeout(10)->run("df -h {$webroot} | tail -1 | awk '{print $4}'");
|
|
||||||
if ($result->successful()) {
|
|
||||||
$free = trim($result->output());
|
|
||||||
$this->line(" ✓ Free space: {$free}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check 7: Validate bundled/config gamedata files
|
|
||||||
$this->addCheck(function () {
|
|
||||||
$bundledConfigDir = '/var/www/Gamedata/bundled/config';
|
|
||||||
$configDir = '/var/www/Gamedata/config';
|
|
||||||
$requiredFiles = ['HabboAvatarActions.json', 'FurnitureData.json', 'ExternalTexts.json', 'ProductData.json', 'FigureData.json', 'FigureMap.json', 'EffectMap.json'];
|
|
||||||
|
|
||||||
// Create bundled/config if it doesn't exist
|
|
||||||
if (! is_dir($bundledConfigDir)) {
|
|
||||||
mkdir($bundledConfigDir, 0755, true);
|
|
||||||
$this->line(" 📁 Created directory: {$bundledConfigDir}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy missing files from config to bundled/config
|
|
||||||
foreach ($requiredFiles as $file) {
|
|
||||||
$targetPath = $bundledConfigDir . '/' . $file;
|
|
||||||
if (! file_exists($targetPath)) {
|
|
||||||
$sourcePath = $configDir . '/' . $file;
|
|
||||||
if (file_exists($sourcePath)) {
|
|
||||||
copy($sourcePath, $targetPath);
|
|
||||||
$this->line(" 📋 Copied: {$file} to bundled/config");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line(' ✓ Gamedata bundled/config files ready');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check 8: Validate existing config files
|
|
||||||
$this->addCheck(function () use ($webroot) {
|
|
||||||
$configFiles = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
|
||||||
foreach ($configFiles as $file) {
|
|
||||||
$path = $webroot . '/' . $file;
|
|
||||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
|
||||||
if ($result->exitCode() === 0) {
|
|
||||||
// Validate JSON
|
|
||||||
$jsonCheck = Process::timeout(5)->run('python3 -c "import json; json.load(open(\'' . $path . '\'))" 2>&1 || echo "INVALID"');
|
|
||||||
if (str_contains($jsonCheck->output(), 'INVALID')) {
|
|
||||||
$this->warn(" ⚠ Invalid JSON: {$file}");
|
|
||||||
} else {
|
|
||||||
$this->line(" ✓ Valid JSON: {$file}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->line(" - New file: {$file}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run all checks
|
|
||||||
$this->line('');
|
|
||||||
$this->info('Running pre-flight checks...');
|
|
||||||
$this->line('───────────────────────────────────────────────');
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach ($this->checks as $check) {
|
|
||||||
$check();
|
|
||||||
}
|
|
||||||
$this->line('───────────────────────────────────────────────');
|
|
||||||
$this->info('✅ All checks passed!');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('❌ Check failed: ' . $e->getMessage());
|
|
||||||
$this->newLine();
|
|
||||||
$this->error('Please fix the issue before generating configs.');
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate configs
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Generating configuration files...');
|
|
||||||
$this->line('───────────────────────────────────────────────');
|
|
||||||
|
|
||||||
$protocol = str_starts_with((string) $siteUrl, 'https') ? 'https' : 'http';
|
|
||||||
$host = preg_replace('/^https?:\/\//', '', (string) $siteUrl);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Step 0: Sync latest example files from GitHub
|
|
||||||
$this->syncExampleFromGithub($buildPath, $webroot);
|
|
||||||
|
|
||||||
$rendererConfig = $this->generateRendererConfig($protocol, $host, $buildPath, $webroot);
|
|
||||||
$uiConfig = $this->generateUiConfig($protocol, $host, $buildPath, $webroot);
|
|
||||||
|
|
||||||
// Compare with existing deployed configs
|
|
||||||
$this->compareConfigs($rendererConfig, 'renderer-config', $webroot);
|
|
||||||
$this->compareConfigs($uiConfig, 'ui-config', $webroot);
|
|
||||||
|
|
||||||
// Validate generated JSON
|
|
||||||
$this->validateJson($rendererConfig, 'renderer-config');
|
|
||||||
$this->validateJson($uiConfig, 'ui-config');
|
|
||||||
|
|
||||||
$tempFile = '/tmp/nitro_config_' . uniqid();
|
|
||||||
file_put_contents($tempFile . '_renderer', json_encode($rendererConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
||||||
file_put_contents($tempFile . '_ui', json_encode($uiConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
||||||
|
|
||||||
// Copy generated configs
|
|
||||||
$commands = [
|
|
||||||
'cp "' . $tempFile . '_renderer" "' . $webroot . '/renderer-config.json"',
|
|
||||||
'cp "' . $tempFile . '_ui" "' . $webroot . '/ui-config.json"',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Copy UITexts.json if it exists (check both buildPath and webroot)
|
|
||||||
$uitextsInBuild = Process::timeout(5)->run('test -f "' . $buildPath . '/UITexts.example"')->exitCode() === 0;
|
|
||||||
$uitextsInWebroot = Process::timeout(5)->run('test -f "' . $webroot . '/UITexts.example"')->exitCode() === 0;
|
|
||||||
if ($uitextsInBuild) {
|
|
||||||
$commands[] = 'cp "' . $buildPath . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
|
||||||
$this->line(' ✓ Generated: UITexts.json (from build path)');
|
|
||||||
} elseif ($uitextsInWebroot) {
|
|
||||||
$commands[] = 'cp "' . $webroot . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
|
||||||
$this->line(' ✓ Generated: UITexts.json (from webroot)');
|
|
||||||
}
|
|
||||||
|
|
||||||
$commands[] = 'rm "' . $tempFile . '_renderer" "' . $tempFile . '_ui"';
|
|
||||||
|
|
||||||
Process::timeout(10)->run(implode(' && ', $commands));
|
|
||||||
|
|
||||||
// Set proper ownership
|
|
||||||
Process::timeout(10)->run("chown www-data:www-data {$webroot}/renderer-config.json {$webroot}/ui-config.json {$webroot}/UITexts.json 2>/dev/null");
|
|
||||||
|
|
||||||
setting('nitro_config_generated_at', now()->toIso8601String());
|
|
||||||
|
|
||||||
$this->line(' ✓ Generated: renderer-config.json');
|
|
||||||
$this->line(' ✓ Generated: ui-config.json');
|
|
||||||
|
|
||||||
// Verify generated files
|
|
||||||
$this->verifyGeneratedFiles($webroot);
|
|
||||||
|
|
||||||
$this->line('───────────────────────────────────────────────');
|
|
||||||
$this->info('✅ Configs generated successfully!');
|
|
||||||
|
|
||||||
Log::info('[NitroConfig] Generated successfully', [
|
|
||||||
'webroot' => $webroot,
|
|
||||||
'host' => $host,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('❌ Generation failed: ' . $e->getMessage());
|
|
||||||
Log::error('[NitroConfig] Generation failed', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addCheck(callable $check): void
|
|
||||||
{
|
|
||||||
$this->checks[] = function () use ($check) {
|
|
||||||
$check();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateJson(array $data, string $name): void
|
|
||||||
{
|
|
||||||
$encoded = json_encode($data, JSON_THROW_ON_ERROR);
|
|
||||||
$decoded = json_decode($encoded, true);
|
|
||||||
|
|
||||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
throw new \Exception("Invalid JSON generated for {$name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line(" ✓ Validated: {$name}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private function verifyGeneratedFiles(string $webroot): void
|
|
||||||
{
|
|
||||||
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
|
||||||
$allValid = true;
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$path = $webroot . '/' . $file;
|
|
||||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
|
||||||
|
|
||||||
if ($result->exitCode() === 0) {
|
|
||||||
$size = Process::timeout(5)->run('stat -c%s ' . escapeshellarg($path));
|
|
||||||
$sizeStr = trim($size->output()) . ' bytes';
|
|
||||||
$this->line(" ✓ Verified: {$file} ({$sizeStr})");
|
|
||||||
} else {
|
|
||||||
$this->warn(" ⚠ Missing: {$file}");
|
|
||||||
$allValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $allValid) {
|
|
||||||
throw new \Exception('Some files were not generated');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateRendererConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
|
||||||
{
|
|
||||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
|
||||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
|
||||||
$wsHost = 'ws.' . $host;
|
|
||||||
|
|
||||||
$rendererConfig = [];
|
|
||||||
|
|
||||||
// Check build path first, then webroot for .example file
|
|
||||||
$examplePath = $buildPath . '/renderer-config.example';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
|
||||||
$examplePath = $webroot . '/renderer-config.example';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to .json version (newer Nitro-V3 format)
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
|
||||||
$jsonPath = $buildPath . '/renderer-config.json';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
|
||||||
$examplePath = $jsonPath;
|
|
||||||
} else {
|
|
||||||
$jsonPath = $webroot . '/renderer-config.json';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
|
||||||
$examplePath = $jsonPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load COMPLETE example file as base
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
|
||||||
$content = file_get_contents($examplePath);
|
|
||||||
// Fix invalid escape sequences (literal \n, \r, \t in JSON values)
|
|
||||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
|
||||||
$rendererConfig = @json_decode($content, true) ?: [];
|
|
||||||
$this->line(' ✓ Loaded renderer config from: ' . basename($examplePath) . ' (' . count($rendererConfig) . ' keys)');
|
|
||||||
} else {
|
|
||||||
$this->warn(' ⚠ renderer-config.example/json not found');
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively replace ALL URLs in the entire config
|
|
||||||
$rendererConfig = $this->replaceUrlsRecursively($rendererConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
|
||||||
|
|
||||||
// Auto-detect asset directory names on disk and fix paths
|
|
||||||
$rendererConfig = $this->autoDetectAssetPaths($rendererConfig, $webroot);
|
|
||||||
|
|
||||||
// Special handling for socket.url - use ws subdomain
|
|
||||||
if (isset($rendererConfig['socket.url'])) {
|
|
||||||
$rendererConfig['socket.url'] = $wssProtocol . '://ws.' . $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-detect local asset paths from Gamedata directory
|
|
||||||
$gamedataBase = $httpProtocol . '://' . $host;
|
|
||||||
|
|
||||||
// Check what directories exist in Gamedata and set URLs accordingly
|
|
||||||
$gamedataPath = '/var/www/Gamedata';
|
|
||||||
if (is_dir($gamedataPath)) {
|
|
||||||
// Check for bundled directory - serve via /gamedata/bundled (nginx alias)
|
|
||||||
if (is_dir($gamedataPath . '/bundled')) {
|
|
||||||
$rendererConfig['asset.url'] = $gamedataBase . '/gamedata/bundled';
|
|
||||||
}
|
|
||||||
// JSON config files are in /gamedata/config/
|
|
||||||
if (is_dir($gamedataPath . '/config')) {
|
|
||||||
$rendererConfig['gamedata.url'] = $gamedataBase . '/gamedata/config';
|
|
||||||
}
|
|
||||||
// Check for c_images directory - serve via /gamedata/c_images
|
|
||||||
if (is_dir($gamedataPath . '/c_images')) {
|
|
||||||
$rendererConfig['image.library.url'] = $gamedataBase . '/gamedata/c_images/';
|
|
||||||
// Use icons folder for furni icons
|
|
||||||
if (is_dir($gamedataPath . '/icons')) {
|
|
||||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/icons';
|
|
||||||
} else {
|
|
||||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/c_images/dcr/hof_furni';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fix furni icon path - icons are directly in hof.furni folder, not in icons subfolder
|
|
||||||
if (isset($rendererConfig['furni.asset.icon.url'])) {
|
|
||||||
$rendererConfig['furni.asset.icon.url'] = '${hof.furni.url}/%libname%%param%_icon.png';
|
|
||||||
}
|
|
||||||
// Fix sound machine samples path - sounds are in /gamedata/sounds/
|
|
||||||
if (isset($rendererConfig['external.samples.url'])) {
|
|
||||||
$rendererConfig['external.samples.url'] = $gamedataBase . '/gamedata/sounds/sound_machine_sample_%sample%.mp3';
|
|
||||||
}
|
|
||||||
// Check for images directory
|
|
||||||
if (is_dir($gamedataPath . '/images')) {
|
|
||||||
$rendererConfig['images.url'] = $gamedataBase . '/gamedata/images';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing keys that might not be in the example
|
|
||||||
if (! isset($rendererConfig['external.plugins'])) {
|
|
||||||
$rendererConfig['external.plugins'] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add YouTube API key from settings
|
|
||||||
$youtubeApiKey = setting('youtube_api_key', '');
|
|
||||||
if (! empty($youtubeApiKey)) {
|
|
||||||
$rendererConfig['youtube.api.key'] = $youtubeApiKey;
|
|
||||||
$this->line(' ✓ Added YouTube API key to renderer config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure pet.types matches the exact required list in order
|
|
||||||
if (isset($rendererConfig['pet.types'])) {
|
|
||||||
$requiredPetTypes = [
|
|
||||||
'dog',
|
|
||||||
'cat',
|
|
||||||
'croco',
|
|
||||||
'terrier',
|
|
||||||
'bear',
|
|
||||||
'pig',
|
|
||||||
'lion',
|
|
||||||
'rhino',
|
|
||||||
'spider',
|
|
||||||
'turtle',
|
|
||||||
'chicken',
|
|
||||||
'frog',
|
|
||||||
'dragon',
|
|
||||||
'monster',
|
|
||||||
'monkey',
|
|
||||||
'horse',
|
|
||||||
'monsterplant',
|
|
||||||
'bunnyeaster',
|
|
||||||
'bunnyevil',
|
|
||||||
'bunnydepressed',
|
|
||||||
'bunnylove',
|
|
||||||
'pigeongood',
|
|
||||||
'pigeonevil',
|
|
||||||
'demonmonkey',
|
|
||||||
'bearbaby',
|
|
||||||
'terrierbaby',
|
|
||||||
'gnome',
|
|
||||||
'leprechaun',
|
|
||||||
'kittenbaby',
|
|
||||||
'puppybaby',
|
|
||||||
'pigletbaby',
|
|
||||||
'haloompa',
|
|
||||||
'fools',
|
|
||||||
'pterosaur',
|
|
||||||
'velociraptor',
|
|
||||||
'cow',
|
|
||||||
'dragondog',
|
|
||||||
'pkmshaymin2',
|
|
||||||
'LeetEendjes',
|
|
||||||
'pkmnentei',
|
|
||||||
'squirtle',
|
|
||||||
'LeetBH',
|
|
||||||
'LeetCaviaaa',
|
|
||||||
'LeetFantj',
|
|
||||||
'LeetHotelMario',
|
|
||||||
'LeetUil',
|
|
||||||
'LeetWolf',
|
|
||||||
'pokemon_mewblu',
|
|
||||||
'LeetBB',
|
|
||||||
'LeetHotelMari1',
|
|
||||||
'LeetMewtw',
|
|
||||||
'LeetPikachu',
|
|
||||||
'LeetYos',
|
|
||||||
'LeetE',
|
|
||||||
'LeetMewt1',
|
|
||||||
'LeetPen',
|
|
||||||
'slendermn',
|
|
||||||
'pkmnPAPI0',
|
|
||||||
'pkmnPAPI1',
|
|
||||||
'pkmnPAPI2',
|
|
||||||
'pkmnPAPI3',
|
|
||||||
'pkmnPAPI4',
|
|
||||||
'pokmn_mew',
|
|
||||||
'pkmashhhpet',
|
|
||||||
'pkmbeautfly',
|
|
||||||
'pkmcelebipe',
|
|
||||||
'pkmdarkraip',
|
|
||||||
'pkmeeveepet',
|
|
||||||
'pkmjirachip',
|
|
||||||
'pkmpichupet',
|
|
||||||
'pkmriolupet',
|
|
||||||
'pkmshayminp',
|
|
||||||
'pkmtogepipe',
|
|
||||||
'pkmvictinip',
|
|
||||||
'slenderm1',
|
|
||||||
'LeetEendj16',
|
|
||||||
'pkmnente1',
|
|
||||||
'LeetBa',
|
|
||||||
'babymeisje',
|
|
||||||
'babyBH',
|
|
||||||
'bb_hbx',
|
|
||||||
'LeetUi1',
|
|
||||||
];
|
|
||||||
|
|
||||||
$rendererConfig['pet.types'] = $requiredPetTypes;
|
|
||||||
$this->line(' ✓ Set pet.types to exact required list');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rendererConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateUiConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
|
||||||
{
|
|
||||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
|
||||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
|
||||||
$wsHost = 'ws.' . $host;
|
|
||||||
|
|
||||||
$uiConfig = [];
|
|
||||||
|
|
||||||
// Check build path first, then webroot for .example file
|
|
||||||
$examplePath = $buildPath . '/ui-config.example';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
|
||||||
$examplePath = $webroot . '/ui-config.example';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to .json version (newer Nitro-V3 format)
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
|
||||||
$jsonPath = $buildPath . '/ui-config.json';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
|
||||||
$examplePath = $jsonPath;
|
|
||||||
} else {
|
|
||||||
$jsonPath = $webroot . '/ui-config.json';
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
|
||||||
$examplePath = $jsonPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load COMPLETE example file as base
|
|
||||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
|
||||||
$content = file_get_contents($examplePath);
|
|
||||||
// Fix invalid escape sequences
|
|
||||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
|
||||||
$uiConfig = @json_decode($content, true) ?: [];
|
|
||||||
$this->line(' ✓ Loaded ui config from: ' . basename($examplePath) . ' (' . count($uiConfig) . ' keys)');
|
|
||||||
} else {
|
|
||||||
$this->warn(' ⚠ ui-config.example/json not found');
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively replace ALL URLs in the entire config
|
|
||||||
$uiConfig = $this->replaceUrlsRecursively($uiConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
|
||||||
|
|
||||||
return $uiConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fixInvalidJsonEscapeSequences(string $content): string
|
|
||||||
{
|
|
||||||
// The example file has literal backslash + any letter in JSON values
|
|
||||||
// which breaks JSON. We need to escape all of these.
|
|
||||||
|
|
||||||
$backslash = chr(92);
|
|
||||||
$letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
|
|
||||||
|
|
||||||
foreach ($letters as $letter) {
|
|
||||||
$content = str_replace($backslash . $letter, $backslash . $backslash . $letter, $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function replaceUrlsRecursively(array $config, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): array
|
|
||||||
{
|
|
||||||
foreach ($config as &$value) {
|
|
||||||
if (is_array($value)) {
|
|
||||||
// Handle nested arrays (like navigator.room.models)
|
|
||||||
if ($this->isAssociativeArray($value)) {
|
|
||||||
$value = $this->replaceUrlsRecursively($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
|
||||||
} else {
|
|
||||||
// Handle arrays of URLs
|
|
||||||
$value = array_map(function ($item) use ($httpProtocol, $wsProtocol, $host, $wsHost) {
|
|
||||||
if (is_string($item)) {
|
|
||||||
return $this->replaceUrl($item, $httpProtocol, $wsProtocol, $host, $wsHost);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $item;
|
|
||||||
}, $value);
|
|
||||||
}
|
|
||||||
} elseif (is_string($value)) {
|
|
||||||
$value = $this->replaceUrl($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function replaceUrl(string $url, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): string
|
|
||||||
{
|
|
||||||
// Replace ws/wss URLs with ws subdomain
|
|
||||||
if (str_starts_with($url, 'ws://') || str_starts_with($url, 'wss://')) {
|
|
||||||
$path = parse_url($url, PHP_URL_PATH) ?? '/';
|
|
||||||
|
|
||||||
return $wsProtocol . '://' . $wsHost . ($path !== '/' ? $path : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace localhost in all URLs
|
|
||||||
$url = preg_replace('#https?://localhost(?::\d+)?#', $httpProtocol . '://' . $host, $url);
|
|
||||||
$url = preg_replace('#wss?://localhost(?::\d+)?#', $wsProtocol . '://' . $wsHost, (string) $url);
|
|
||||||
$url = preg_replace('#localhost(?::\d+)?#', $host, (string) $url);
|
|
||||||
|
|
||||||
// Fix broken escape sequences in URL paths (from invalid JSON in example files)
|
|
||||||
// Pattern: /public\nitro-assets\gamedata or any variation
|
|
||||||
$url = preg_replace('#/public\\n[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
|
||||||
$url = preg_replace('#/public\\\\[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
|
||||||
$url = preg_replace('#/nitro-assets\\\\gamedata#i', '/gamedata', (string) $url);
|
|
||||||
$url = preg_replace('#/nitro\\\\[a-z_-]+#i', '/gamedata', (string) $url);
|
|
||||||
|
|
||||||
// Fix known asset path patterns
|
|
||||||
$url = str_replace('/public/nitro-assets/gamedata', '/gamedata', $url);
|
|
||||||
$url = str_replace('/swf/gamedata', '/gamedata', $url);
|
|
||||||
|
|
||||||
// Clean up any remaining backslashes in URLs
|
|
||||||
$url = str_replace('\\', '/', $url);
|
|
||||||
|
|
||||||
// Clean up any remaining double slashes (but keep protocol slashes)
|
|
||||||
$url = preg_replace('#([^:])//+#', '$1/', $url);
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isAssociativeArray(array $arr): bool
|
|
||||||
{
|
|
||||||
if ($arr === []) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function autoDetectAssetPaths(array $config, string $webroot): array
|
|
||||||
{
|
|
||||||
// Check multiple possible gamedata locations
|
|
||||||
$possiblePaths = [
|
|
||||||
$webroot . '/gamedata',
|
|
||||||
'/var/www/Gamedata',
|
|
||||||
'/var/www/gamedata',
|
|
||||||
];
|
|
||||||
$gamedataPath = array_find($possiblePaths, fn ($path) => is_dir($path));
|
|
||||||
|
|
||||||
if (! $gamedataPath) {
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
$assetDir = opendir($gamedataPath);
|
|
||||||
$actualDirs = [];
|
|
||||||
while (($entry = readdir($assetDir)) !== false) {
|
|
||||||
if ($entry !== '.' && $entry !== '..' && is_dir($gamedataPath . '/' . $entry)) {
|
|
||||||
$actualDirs[strtolower($entry)] = $entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closedir($assetDir);
|
|
||||||
|
|
||||||
$pathChecks = [
|
|
||||||
'pet.asset.url' => 'pets',
|
|
||||||
'furni.asset.url' => 'furniture',
|
|
||||||
'avatar.asset.url' => 'clothes',
|
|
||||||
'avatar.asset.effect.url' => 'effect',
|
|
||||||
'generic.asset.url' => 'generic_custom',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($config as $key => &$value) {
|
|
||||||
if (! is_string($value) || ! isset($pathChecks[$key])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$expectedDir = $pathChecks[$key];
|
|
||||||
$lowerExpected = strtolower($expectedDir);
|
|
||||||
$actualName = null;
|
|
||||||
|
|
||||||
// Special case: "figure" is often used instead of "clothes" for avatars
|
|
||||||
if ($lowerExpected === 'clothes' && isset($actualDirs['figure'])) {
|
|
||||||
$actualName = $actualDirs['figure'];
|
|
||||||
} elseif (isset($actualDirs[$lowerExpected])) {
|
|
||||||
$actualName = $actualDirs[$lowerExpected];
|
|
||||||
} else {
|
|
||||||
foreach ($actualDirs as $actualLower => $actual) {
|
|
||||||
if (str_starts_with($actualLower, rtrim($lowerExpected, 's')) ||
|
|
||||||
str_starts_with(rtrim($actualLower, 's'), rtrim($lowerExpected, 's'))) {
|
|
||||||
$actualName = $actual;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($actualName && $actualName !== $expectedDir) {
|
|
||||||
$value = str_replace("/{$expectedDir}/", "/{$actualName}/", $value);
|
|
||||||
$this->line(" 🔍 Auto-detected: {$key} -> /{$actualName}/ (was /{$expectedDir}/)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function syncExampleFromGithub(string $buildPath, string $webroot): void
|
|
||||||
{
|
|
||||||
$this->info('Syncing latest examples from GitHub...');
|
|
||||||
|
|
||||||
$rendererRepo = setting('nitro_github_url', '');
|
|
||||||
$repo = $this->parseRepoFromUrl($rendererRepo);
|
|
||||||
if (! $repo) {
|
|
||||||
$this->warn(' ⚠ No GitHub repo configured, skipping sync');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$branch = setting('nitro_github_branch', 'main');
|
|
||||||
|
|
||||||
$examples = [
|
|
||||||
'renderer-config.example' => 'renderer-config.example',
|
|
||||||
'ui-config.example' => 'ui-config.example',
|
|
||||||
'UITexts.example' => 'UITexts.json',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach (array_keys($examples) as $remoteFile) {
|
|
||||||
$tempFile = '/tmp/' . $remoteFile . '_' . uniqid();
|
|
||||||
$fetched = false;
|
|
||||||
|
|
||||||
// Try multiple paths: root, public/, nitro-client/dist/, Nitro-V3 paths
|
|
||||||
$paths = ['', 'public/', 'nitro-client/dist/', 'dist/', 'src/', 'assets/'];
|
|
||||||
foreach ($paths as $path) {
|
|
||||||
$url = "https://raw.githubusercontent.com/{$repo}/{$branch}/{$path}{$remoteFile}";
|
|
||||||
$result = Process::timeout(15)->run("curl -sL -o {$tempFile} '{$url}'");
|
|
||||||
|
|
||||||
if ($result->successful() && file_exists($tempFile) && filesize($tempFile) > 10) {
|
|
||||||
$content = file_get_contents($tempFile);
|
|
||||||
$data = @json_decode($content, true);
|
|
||||||
|
|
||||||
if (is_array($data) && $data !== []) {
|
|
||||||
// Save to both buildPath and webroot
|
|
||||||
file_put_contents($buildPath . '/' . $remoteFile, $content);
|
|
||||||
file_put_contents($webroot . '/' . $remoteFile, $content);
|
|
||||||
$this->line(" ✓ Synced: {$remoteFile} (" . count($data) . " keys from {$path}{$remoteFile})");
|
|
||||||
$fetched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@unlink($tempFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $fetched) {
|
|
||||||
$this->line(" - Skipped: {$remoteFile} (not found in any path)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseRepoFromUrl(string $url): ?string
|
|
||||||
{
|
|
||||||
if (preg_match('/github\.com\/([^\/]+\/[^\/\?#]+)/', $url, $matches)) {
|
|
||||||
return rtrim($matches[1], '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function compareConfigs(array $generated, string $name, string $webroot): void
|
|
||||||
{
|
|
||||||
$currentPath = $webroot . '/' . $name . '.json';
|
|
||||||
if (! file_exists($currentPath)) {
|
|
||||||
$this->line(" ℹ {$name}: Geen bestaande config — nieuwe generatie");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$current = @json_decode(file_get_contents($currentPath), true);
|
|
||||||
if (! is_array($current)) {
|
|
||||||
$this->warn(" ⚠ {$name}: Bestaande config is ongeldig JSON");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$newKeys = array_diff(array_keys($generated), array_keys($current));
|
|
||||||
$removedKeys = array_diff(array_keys($current), array_keys($generated));
|
|
||||||
$changedKeys = [];
|
|
||||||
|
|
||||||
foreach (array_intersect(array_keys($generated), array_keys($current)) as $key) {
|
|
||||||
if ($generated[$key] !== $current[$key]) {
|
|
||||||
$changedKeys[] = $key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($newKeys !== []) {
|
|
||||||
$this->line(" 🆕 {$name}: " . count($newKeys) . ' nieuwe key(s): ' . implode(', ', array_slice($newKeys, 0, 10)));
|
|
||||||
if (count($newKeys) > 10) {
|
|
||||||
$this->line(' ... en ' . (count($newKeys) - 10) . ' meer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($removedKeys !== []) {
|
|
||||||
$this->line(" 🗑 {$name}: " . count($removedKeys) . ' verwijderde key(s): ' . implode(', ', array_slice($removedKeys, 0, 5)));
|
|
||||||
}
|
|
||||||
if ($changedKeys !== []) {
|
|
||||||
$this->line(" 🔄 {$name}: " . count($changedKeys) . ' gewijzigde key(s): ' . implode(', ', array_slice($changedKeys, 0, 5)));
|
|
||||||
}
|
|
||||||
if ($newKeys === [] && $removedKeys === [] && $changedKeys === []) {
|
|
||||||
$this->line(" ✓ {$name}: Geen wijzigingen");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Enums\AlertChannel;
|
|
||||||
use App\Enums\AlertType;
|
|
||||||
use App\Services\AlertService;
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class NitroUpdateCommand extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'nitro:auto
|
|
||||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
|
||||||
{--build-only : Alleen build en deploy uitvoeren}
|
|
||||||
{--full : Volledige reset en reinstall}
|
|
||||||
{--repair : Probeer Nitro te repareren}
|
|
||||||
{--diagnose : Toon diagnose informatie}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Automatische Nitro client en renderer updates';
|
|
||||||
|
|
||||||
private const string CACHE_KEY_LAST_UPDATE = 'nitro_last_update';
|
|
||||||
|
|
||||||
public function handle(AlertService $alertService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->info('Nitro update check...');
|
|
||||||
|
|
||||||
if ($this->option('diagnose')) {
|
|
||||||
return $this->diagnose($nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->option('repair')) {
|
|
||||||
return $this->repair($alertService, $nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
$isForced = $this->option('force');
|
|
||||||
$buildOnly = $this->option('build-only');
|
|
||||||
$full = $this->option('full');
|
|
||||||
|
|
||||||
if ($buildOnly) {
|
|
||||||
return $this->buildOnly($alertService, $nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $isForced && ! $this->isScheduledTime()) {
|
|
||||||
$this->line('Niet de geplande tijd, overslaan.');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$repo = setting('nitro_github_url', 'duckietm/Nitro-V3 (default)');
|
|
||||||
$this->info("GitHub: {$repo}");
|
|
||||||
|
|
||||||
if ($full) {
|
|
||||||
return $this->fullReinstall($alertService, $nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = $nitroService->getStatus();
|
|
||||||
|
|
||||||
$this->info('Client: ' . ($status['client_installed'] ? '✅' : '❌'));
|
|
||||||
$this->info('Renderer: ' . ($status['renderer_installed'] ? '✅' : '❌'));
|
|
||||||
$this->info('Build: ' . ($status['build_exists'] ? '✅' : '❌'));
|
|
||||||
$this->info('Deployed: ' . ($status['deployed'] ? '✅' : '❌'));
|
|
||||||
|
|
||||||
if (! $status['client_installed'] || ! $status['renderer_installed']) {
|
|
||||||
$this->warn('Nitro niet volledig geïnstalleerd. Voer --full uit voor volledige installatie.');
|
|
||||||
if ($this->confirm('Wil je nu volledig installeren?')) {
|
|
||||||
return $this->fullReinstall($alertService, $nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line('✅ Nitro client is up-to-date.');
|
|
||||||
|
|
||||||
if ($isForced) {
|
|
||||||
$this->warn('Force update aangevraagd...');
|
|
||||||
|
|
||||||
return $this->fullReinstall($alertService, $nitroService);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::put(self::CACHE_KEY_LAST_UPDATE, now()->toIso8601String());
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function diagnose(NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->info('🔍 Nitro diagnose...');
|
|
||||||
$diagnosis = $nitroService->diagnose();
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('=== Controle Resultaten ===');
|
|
||||||
|
|
||||||
$checks = $diagnosis['checks'] ?? [];
|
|
||||||
foreach ($checks as $key => $value) {
|
|
||||||
if (is_bool($value)) {
|
|
||||||
$this->line(($value ? '✅' : '❌') . ' ' . $key);
|
|
||||||
} elseif (is_array($value)) {
|
|
||||||
$this->line($key . ': ' . count($value) . ' items');
|
|
||||||
} else {
|
|
||||||
$this->line($key . ': ' . $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($diagnosis['issues'])) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->error('=== Problemen ===');
|
|
||||||
foreach ($diagnosis['issues'] as $issue) {
|
|
||||||
$this->line('❌ ' . $issue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($diagnosis['recommendations'])) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('=== Aanbevelingen ===');
|
|
||||||
foreach ($diagnosis['recommendations'] as $rec) {
|
|
||||||
$this->line('💡 ' . $rec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function repair(AlertService $alertService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->warn('🔧 Nitro repair modus gestart...');
|
|
||||||
$this->info('Dit zal de Nitro installatie controleren en repareren.');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$repairResult = $nitroService->repair();
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$this->info('✅ Repair succesvol!');
|
|
||||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_UPDATE,
|
|
||||||
'Nitro client gerepareerd',
|
|
||||||
['actions' => $repairResult['actions'] ?? []],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
|
||||||
if (! empty($repairResult['actions'])) {
|
|
||||||
$this->line('Uitgevoerde acties:');
|
|
||||||
foreach ($repairResult['actions'] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (! empty($repairResult['errors'])) {
|
|
||||||
$this->line('Fouten:');
|
|
||||||
foreach ($repairResult['errors'] as $error) {
|
|
||||||
$this->line(' ❌ ' . $error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_ERROR,
|
|
||||||
'Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildOnly(AlertService $alertService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->info('Build en deploy uitvoeren...');
|
|
||||||
|
|
||||||
$buildResult = $nitroService->buildClient();
|
|
||||||
|
|
||||||
if (! $buildResult['success']) {
|
|
||||||
$this->error('Build mislukt: ' . ($buildResult['error'] ?? 'Onbekend'));
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$nitroService->deployClient();
|
|
||||||
$nitroService->generateConfigs();
|
|
||||||
|
|
||||||
$this->info('Client succesvol gedeployed!');
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_UPDATE,
|
|
||||||
'Nitro client opnieuw gedeployed',
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fullReinstall(AlertService $alertService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->warn('Volledige reinstall wordt uitgevoerd...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $nitroService->updateNitro();
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$this->info('Nitro succesvol opnieuw geïnstalleerd!');
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::EMULATOR_UPDATE,
|
|
||||||
'Nitro client succesvol geüpdatet en gedeployed',
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error('Reinstall mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
|
||||||
$alertService->send(
|
|
||||||
AlertType::CRITICAL_ERROR,
|
|
||||||
'Nitro Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
|
||||||
[],
|
|
||||||
AlertChannel::DISCORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('Reinstall exception: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isScheduledTime(): bool
|
|
||||||
{
|
|
||||||
$scheduleTime = setting('nitro_auto_update_schedule', '03:00');
|
|
||||||
$scheduleDays = setting('nitro_auto_update_days', '0,6');
|
|
||||||
$enabled = setting('nitro_auto_update_enabled', false);
|
|
||||||
|
|
||||||
if (! $enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = now();
|
|
||||||
$currentTime = $now->format('H:i');
|
|
||||||
$currentDay = (int) $now->dayOfWeek;
|
|
||||||
|
|
||||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
|
||||||
|
|
||||||
if (! in_array($currentDay, $allowedDays)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($currentTime !== $scheduleTime) {
|
|
||||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
|
||||||
if ($minuteDiff > 5) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use App\Services\SettingsService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class SwitchNitroBranch extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'app:switch-nitro-branch {--branch=main}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Switch Nitro to a specific branch (runs in background)';
|
|
||||||
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$branch = $this->option('branch') ?? 'main';
|
|
||||||
|
|
||||||
$this->info("🔄 Switching Nitro to branch: {$branch}");
|
|
||||||
|
|
||||||
try {
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
$result = $nitroService->updateNitro(true);
|
|
||||||
|
|
||||||
if ($result['success'] ?? false) {
|
|
||||||
$this->info("✅ Switched to {$branch} successfully!");
|
|
||||||
$this->info($result['message'] ?? '');
|
|
||||||
Log::info('[NitroSwitch] Success', ['branch' => $branch, 'message' => $result['message'] ?? '']);
|
|
||||||
} else {
|
|
||||||
$this->error('❌ Switch failed: ' . ($result['error'] ?? 'Unknown error'));
|
|
||||||
Log::error('[NitroSwitch] Failed', ['branch' => $branch, 'error' => $result['error'] ?? 'Unknown']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::forget('website_settings');
|
|
||||||
SettingsService::clearCache();
|
|
||||||
|
|
||||||
return ($result['success'] ?? false) ? 0 : 1;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error('❌ Exception: ' . $e->getMessage());
|
|
||||||
Log::error('[NitroSwitch] Exception', ['branch' => $branch, 'error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class SystemHealthCommand extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'system:health
|
|
||||||
{--json : Output als JSON}
|
|
||||||
{--details : Toon meer details}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Controleer systeem gezondheid';
|
|
||||||
|
|
||||||
public function handle(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$json = $this->option('json');
|
|
||||||
$this->option('details');
|
|
||||||
|
|
||||||
$health = $this->performHealthCheck($emuService, $nitroService);
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
$this->line(json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
||||||
|
|
||||||
return $health['status'] === 'healthy' ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->displayHealthCheck($health);
|
|
||||||
|
|
||||||
return $health['status'] === 'healthy' ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function performHealthCheck(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): array
|
|
||||||
{
|
|
||||||
$checks = [];
|
|
||||||
$issues = [];
|
|
||||||
$warnings = [];
|
|
||||||
|
|
||||||
$checks['timestamp'] = now()->toIso8601String();
|
|
||||||
|
|
||||||
$checks['php'] = [
|
|
||||||
'version' => PHP_VERSION,
|
|
||||||
'status' => 'ok',
|
|
||||||
];
|
|
||||||
|
|
||||||
$checks['system'] = [
|
|
||||||
'os' => PHP_OS,
|
|
||||||
'user' => posix_getpwuid(posix_geteuid())['name'] ?? 'unknown',
|
|
||||||
];
|
|
||||||
|
|
||||||
$diskFree = @disk_free_space('/');
|
|
||||||
$diskTotal = @disk_total_space('/');
|
|
||||||
$diskPercent = $diskTotal > 0 ? round(($diskFree / $diskTotal) * 100, 1) : 0;
|
|
||||||
$checks['disk'] = [
|
|
||||||
'free' => $this->formatBytes($diskFree),
|
|
||||||
'total' => $this->formatBytes($diskTotal),
|
|
||||||
'percent_free' => $diskPercent,
|
|
||||||
'status' => $diskPercent > 10 ? 'ok' : 'critical',
|
|
||||||
];
|
|
||||||
if ($diskPercent < 10) {
|
|
||||||
$issues[] = 'Disk space critically low: ' . $diskPercent . '% free';
|
|
||||||
} elseif ($diskPercent < 20) {
|
|
||||||
$warnings[] = 'Disk space low: ' . $diskPercent . '% free';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$emuDiagnosis = $emuService->diagnose();
|
|
||||||
$checks['emulator'] = [
|
|
||||||
'configured' => $emuDiagnosis['checks']['is_configured'] ?? false,
|
|
||||||
'jar_exists' => $emuDiagnosis['checks']['jar_exists'] ?? false,
|
|
||||||
'service_running' => $emuDiagnosis['checks']['service_running'] ?? false,
|
|
||||||
'db_connected' => $emuDiagnosis['checks']['emulator_db_connected'] ?? false,
|
|
||||||
'status' => empty($emuDiagnosis['issues']) ? 'ok' : 'issues',
|
|
||||||
'issues' => $emuDiagnosis['issues'] ?? [],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (! empty($emuDiagnosis['issues'])) {
|
|
||||||
foreach ($emuDiagnosis['issues'] as $issue) {
|
|
||||||
$issues[] = 'Emulator: ' . $issue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$checks['emulator'] = [
|
|
||||||
'status' => 'error',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
];
|
|
||||||
$issues[] = 'Emulator check failed: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$nitroDiagnosis = $nitroService->diagnose();
|
|
||||||
$checks['nitro'] = [
|
|
||||||
'client_installed' => $nitroDiagnosis['checks']['client_installed'] ?? false,
|
|
||||||
'renderer_installed' => $nitroDiagnosis['checks']['renderer_installed'] ?? false,
|
|
||||||
'deployed' => $nitroDiagnosis['checks']['deployed'] ?? false,
|
|
||||||
'status' => empty($nitroDiagnosis['issues']) ? 'ok' : 'issues',
|
|
||||||
'issues' => $nitroDiagnosis['issues'] ?? [],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (! empty($nitroDiagnosis['issues'])) {
|
|
||||||
foreach ($nitroDiagnosis['issues'] as $issue) {
|
|
||||||
$issues[] = 'Nitro: ' . $issue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$checks['nitro'] = [
|
|
||||||
'status' => 'error',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
];
|
|
||||||
$issues[] = 'Nitro check failed: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sqlDiagnosis = $emuService->diagnoseSqlUpdates();
|
|
||||||
$checks['sql_updates'] = [
|
|
||||||
'table_exists' => $sqlDiagnosis['table_exists'] ?? false,
|
|
||||||
'applied' => $sqlDiagnosis['applied_count'] ?? 0,
|
|
||||||
'pending' => $sqlDiagnosis['pending_count'] ?? 0,
|
|
||||||
'status' => ($sqlDiagnosis['pending_count'] ?? 0) > 0 ? 'pending' : 'ok',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
|
|
||||||
$warnings[] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$checks['sql_updates'] = [
|
|
||||||
'status' => 'error',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$webserverCheck = Process::timeout(5)->run('which nginx || which apache2 || which httpd');
|
|
||||||
$checks['webserver'] = [
|
|
||||||
'installed' => $webserverCheck->successful(),
|
|
||||||
'status' => $webserverCheck->successful() ? 'ok' : 'unknown',
|
|
||||||
];
|
|
||||||
|
|
||||||
$mysqlCheck = Process::timeout(5)->run('which mysql || which mariadb');
|
|
||||||
$checks['database'] = [
|
|
||||||
'client_installed' => $mysqlCheck->successful(),
|
|
||||||
'status' => $mysqlCheck->successful() ? 'ok' : 'unknown',
|
|
||||||
];
|
|
||||||
|
|
||||||
$nodeCheck = Process::timeout(5)->run('which node && which yarn');
|
|
||||||
$checks['node'] = [
|
|
||||||
'installed' => $nodeCheck->successful(),
|
|
||||||
'status' => $nodeCheck->successful() ? 'ok' : 'missing',
|
|
||||||
];
|
|
||||||
if (! $nodeCheck->successful()) {
|
|
||||||
$warnings[] = 'Node.js/Yarn niet geïnstalleerd';
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = $issues === [] ? 'healthy' : ($warnings === [] ? 'warning' : 'degraded');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => $status,
|
|
||||||
'checks' => $checks,
|
|
||||||
'issues' => $issues,
|
|
||||||
'warnings' => $warnings,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function displayHealthCheck(array $health): void
|
|
||||||
{
|
|
||||||
$status = $health['status'];
|
|
||||||
|
|
||||||
$statusIcon = match ($status) {
|
|
||||||
'healthy' => '✅',
|
|
||||||
'warning' => '⚠️',
|
|
||||||
'degraded' => '❌',
|
|
||||||
default => '❓',
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->info("{$statusIcon} Systeem Gezondheid: " . strtoupper($status));
|
|
||||||
$this->line('═══════════════════════════════════════════════');
|
|
||||||
|
|
||||||
$checks = $health['checks'] ?? [];
|
|
||||||
|
|
||||||
$this->line('📦 Systeem:');
|
|
||||||
$this->line(' PHP: ' . ($checks['php']['version'] ?? '?'));
|
|
||||||
$this->line(' User: ' . ($checks['system']['user'] ?? '?'));
|
|
||||||
|
|
||||||
$diskStatus = $checks['disk']['status'] ?? '?';
|
|
||||||
$diskIcon = $diskStatus === 'ok' ? '✅' : '❌';
|
|
||||||
$this->line(" {$diskIcon} Disk: " . ($checks['disk']['percent_free'] ?? '?') . '% vrij');
|
|
||||||
|
|
||||||
$emuStatus = $checks['emulator']['status'] ?? '?';
|
|
||||||
$emuIcon = $emuStatus === 'ok' ? '✅' : '⚠️';
|
|
||||||
$this->line(" {$emuIcon} Emulator: " . ucfirst($emuStatus));
|
|
||||||
|
|
||||||
$nitroStatus = $checks['nitro']['status'] ?? '?';
|
|
||||||
$nitroIcon = $nitroStatus === 'ok' ? '✅' : '⚠️';
|
|
||||||
$this->line(" {$nitroIcon} Nitro: " . ucfirst($nitroStatus));
|
|
||||||
|
|
||||||
$sqlStatus = $checks['sql_updates']['status'] ?? '?';
|
|
||||||
$sqlIcon = $sqlStatus === 'ok' ? '✅' : '⚠️';
|
|
||||||
$this->line(" {$sqlIcon} SQL Updates: " . ucfirst($sqlStatus) . ' (' . ($checks['sql_updates']['applied'] ?? 0) . ' toegepast)');
|
|
||||||
|
|
||||||
$nodeStatus = $checks['node']['status'] ?? '?';
|
|
||||||
$nodeIcon = $nodeStatus === 'ok' ? '✅' : '❌';
|
|
||||||
$this->line(" {$nodeIcon} Node.js: " . ucfirst($nodeStatus));
|
|
||||||
|
|
||||||
if (! empty($health['issues'])) {
|
|
||||||
$this->line('');
|
|
||||||
$this->error('❌ Problemen:');
|
|
||||||
foreach ($health['issues'] as $issue) {
|
|
||||||
$this->line(' - ' . $issue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($health['warnings'])) {
|
|
||||||
$this->line('');
|
|
||||||
$this->warn('⚠️ Waarschuwingen:');
|
|
||||||
foreach ($health['warnings'] as $warning) {
|
|
||||||
$this->line(' - ' . $warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line('═══════════════════════════════════════════════');
|
|
||||||
|
|
||||||
if ($status === 'healthy') {
|
|
||||||
$this->info('✅ Alles werkt correct!');
|
|
||||||
} else {
|
|
||||||
$this->warn('Run "php artisan system:repair" om problemen op te lossen');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function formatBytes(?float $bytes): string
|
|
||||||
{
|
|
||||||
if ($bytes === null) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
|
|
||||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
$i = 0;
|
|
||||||
|
|
||||||
while ($bytes >= 1024 && $i < count($units) - 1) {
|
|
||||||
$bytes /= 1024;
|
|
||||||
$i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return round($bytes, 2) . ' ' . $units[$i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Services\AlertService;
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class SystemRepairCommand extends Command
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected $signature = 'system:repair
|
|
||||||
{--emu : Alleen emulator repareren}
|
|
||||||
{--nitro : Alleen Nitro repareren}
|
|
||||||
{--check : Alleen controleren, niet repareren}
|
|
||||||
{--force : Forceer reparatie ook al is alles OK}
|
|
||||||
{--full : Volledige reset en reinstall}
|
|
||||||
{--nuke : Alles verwijderen en opnieuw installeren (gevaarlijk!)}';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected $description = 'Automatische systeem reparatie voor emulator en Nitro';
|
|
||||||
|
|
||||||
private const string CACHE_KEY_LAST_REPAIR = 'system_last_repair';
|
|
||||||
|
|
||||||
public function handle(AlertService $alertService, EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
|
||||||
{
|
|
||||||
$this->info('🔧 System Repair Service gestart...');
|
|
||||||
$this->line('═══════════════════════════════════════════════');
|
|
||||||
|
|
||||||
$checkOnly = $this->option('check');
|
|
||||||
$force = $this->option('force');
|
|
||||||
$full = $this->option('full');
|
|
||||||
$emuOnly = $this->option('emu');
|
|
||||||
$nitroOnly = $this->option('nitro');
|
|
||||||
|
|
||||||
$results = [
|
|
||||||
'emulator' => null,
|
|
||||||
'nitro' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (! $nitroOnly) {
|
|
||||||
$results['emulator'] = $this->repairEmulator($emuService, $checkOnly, $force, $full);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $emuOnly) {
|
|
||||||
$results['nitro'] = $this->repairNitro($nitroService, $checkOnly, $force, $full);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::put(self::CACHE_KEY_LAST_REPAIR, now()->toIso8601String());
|
|
||||||
|
|
||||||
$emuOk = $results['emulator'] === null || ($results['emulator']['success'] ?? false);
|
|
||||||
$nitroOk = $results['nitro'] === null || ($results['nitro']['success'] ?? false);
|
|
||||||
|
|
||||||
$this->line('═══════════════════════════════════════════════');
|
|
||||||
if ($emuOk && $nitroOk) {
|
|
||||||
$this->info('✅ Alle systemen OK');
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureBaseDirectories(): void
|
|
||||||
{
|
|
||||||
$basePaths = [
|
|
||||||
'/var/www',
|
|
||||||
'/var/www/atomcms',
|
|
||||||
storage_path('app'),
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($basePaths as $path) {
|
|
||||||
if (! is_dir($path)) {
|
|
||||||
@mkdir($path, 0755, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process::timeout(5)->run('chown -R www-data:www-data /var/www/atomcms 2>/dev/null || true');
|
|
||||||
Process::timeout(5)->run('chmod -R 755 /var/www/atomcms 2>/dev/null || true');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function repairEmulator(EmulatorUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
|
||||||
{
|
|
||||||
$this->info('Emulator controleren...');
|
|
||||||
|
|
||||||
$this->ensureBaseDirectories();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$diagnosis = $service->diagnose();
|
|
||||||
|
|
||||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
|
||||||
$this->line(' ✅ Emulator is OK');
|
|
||||||
|
|
||||||
return ['success' => true, 'status' => 'ok'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($checkOnly) {
|
|
||||||
$this->warn(' ⚠️ Emulator problemen gevonden:');
|
|
||||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
|
||||||
$this->line(' - ' . $issue);
|
|
||||||
}
|
|
||||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
|
||||||
$this->line(' 💡 ' . $rec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->warn(' 🔧 Emulator wordt gerepareerd...');
|
|
||||||
|
|
||||||
if ($full) {
|
|
||||||
$this->line(' 📦 Volledige reset...');
|
|
||||||
$repairResult = $service->repairEmulator();
|
|
||||||
} else {
|
|
||||||
$repairResult = $service->repairEmulator();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$this->info(' ✅ Emulator gerepareerd!');
|
|
||||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error(' ❌ Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
|
||||||
if (! empty($repairResult['actions'])) {
|
|
||||||
$this->line(' Uitgevoerde acties:');
|
|
||||||
foreach ($repairResult['actions'] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error(' ❌ Emulator exception: ' . $e->getMessage());
|
|
||||||
Log::error('[SystemRepair] Emulator exception', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function repairNitro(NitroUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
|
||||||
{
|
|
||||||
$this->info('Nitro controleren...');
|
|
||||||
|
|
||||||
$this->ensureBaseDirectories();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$diagnosis = $service->diagnose();
|
|
||||||
|
|
||||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
|
||||||
$this->line(' ✅ Nitro is OK');
|
|
||||||
|
|
||||||
return ['success' => true, 'status' => 'ok'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($checkOnly) {
|
|
||||||
$this->warn(' ⚠️ Nitro problemen gevonden:');
|
|
||||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
|
||||||
$this->line(' - ' . $issue);
|
|
||||||
}
|
|
||||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
|
||||||
$this->line(' 💡 ' . $rec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->warn(' 🔧 Nitro wordt gerepareerd...');
|
|
||||||
|
|
||||||
if ($full) {
|
|
||||||
$this->line(' 📦 Volledige reset...');
|
|
||||||
$repairResult = $service->updateNitro();
|
|
||||||
} else {
|
|
||||||
$repairResult = $service->repair();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$this->info(' ✅ Nitro gerepareerd!');
|
|
||||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->error(' ❌ Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
|
||||||
if (! empty($repairResult['actions'])) {
|
|
||||||
$this->line(' Uitgevoerde acties:');
|
|
||||||
foreach ($repairResult['actions'] as $action) {
|
|
||||||
$this->line(' - ' . $action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error(' ❌ Nitro exception: ' . $e->getMessage());
|
|
||||||
Log::error('[SystemRepair] Nitro exception', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,16 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Console;
|
namespace App\Console;
|
||||||
|
|
||||||
use App\Console\Commands\AutoUpdateCommand;
|
|
||||||
use App\Console\Commands\DDoSDetectionCommand;
|
use App\Console\Commands\DDoSDetectionCommand;
|
||||||
use App\Console\Commands\EmulatorMonitorCommand;
|
use App\Console\Commands\EmulatorMonitorCommand;
|
||||||
use App\Console\Commands\EmulatorUpdateCommand;
|
|
||||||
use App\Console\Commands\FixCodeCommand;
|
use App\Console\Commands\FixCodeCommand;
|
||||||
use App\Console\Commands\GenerateNitroConfigs;
|
|
||||||
use App\Console\Commands\NitroUpdateCommand;
|
|
||||||
use App\Console\Commands\SystemCheckCommand;
|
use App\Console\Commands\SystemCheckCommand;
|
||||||
use App\Console\Commands\SystemHealthCommand;
|
|
||||||
use App\Console\Commands\SystemRepairCommand;
|
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
@@ -28,9 +22,6 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping();
|
$schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping();
|
||||||
$schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping();
|
$schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping();
|
||||||
$schedule->command('monitor:ddos')->everyFiveMinutes()->withoutOverlapping();
|
$schedule->command('monitor:ddos')->everyFiveMinutes()->withoutOverlapping();
|
||||||
$schedule->command('update:auto')->everyMinute()->withoutOverlapping();
|
|
||||||
$schedule->command('nitro:auto')->everyMinute()->withoutOverlapping();
|
|
||||||
$schedule->command('system:repair')->everyTenMinutes()->withoutOverlapping();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
@@ -42,12 +33,6 @@ class Kernel extends ConsoleKernel
|
|||||||
$this->commands[] = FixCodeCommand::class;
|
$this->commands[] = FixCodeCommand::class;
|
||||||
$this->commands[] = EmulatorMonitorCommand::class;
|
$this->commands[] = EmulatorMonitorCommand::class;
|
||||||
$this->commands[] = DDoSDetectionCommand::class;
|
$this->commands[] = DDoSDetectionCommand::class;
|
||||||
$this->commands[] = EmulatorUpdateCommand::class;
|
|
||||||
$this->commands[] = AutoUpdateCommand::class;
|
|
||||||
$this->commands[] = NitroUpdateCommand::class;
|
|
||||||
$this->commands[] = SystemRepairCommand::class;
|
|
||||||
$this->commands[] = SystemHealthCommand::class;
|
|
||||||
$this->commands[] = GenerateNitroConfigs::class;
|
|
||||||
|
|
||||||
require base_path('routes/console.php');
|
require base_path('routes/console.php');
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Executable → Regular
+14
-326
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Pages\Monitoring;
|
namespace App\Filament\Pages\Monitoring;
|
||||||
|
|
||||||
use App\Actions\Commandocentrum\EmulatorControlAction;
|
use App\Actions\Commandocentrum\EmulatorControlAction;
|
||||||
use App\Actions\Commandocentrum\NitroControlAction;
|
|
||||||
use App\Enums\AlertSeverity;
|
use App\Enums\AlertSeverity;
|
||||||
use App\Models\Miscellaneous\WebsitePermission;
|
use App\Models\Miscellaneous\WebsitePermission;
|
||||||
use App\Models\StaffActivity;
|
use App\Models\StaffActivity;
|
||||||
@@ -16,7 +15,6 @@ use App\Services\Diagnostics\DiagnosticRunner;
|
|||||||
use App\Services\GitHubService;
|
use App\Services\GitHubService;
|
||||||
use App\Services\RconService;
|
use App\Services\RconService;
|
||||||
use App\Services\SettingsService;
|
use App\Services\SettingsService;
|
||||||
use App\Services\UpdateHistoryService;
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -106,21 +104,15 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
'emulator_database_username' => $this->getSetting('emulator_database_username', ''),
|
'emulator_database_username' => $this->getSetting('emulator_database_username', ''),
|
||||||
'emulator_database_password' => $this->getSetting('emulator_database_password', ''),
|
'emulator_database_password' => $this->getSetting('emulator_database_password', ''),
|
||||||
'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'),
|
'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'),
|
||||||
'auto_update_enabled' => $this->getSettingBool('auto_update_enabled'),
|
'nitro_emulator_path' => $this->getSetting('nitro_emulator_path', '/var/www/emulator'),
|
||||||
'auto_update_schedule' => $this->getSetting('auto_update_schedule', '03:00'),
|
'nitro_emulator_service' => $this->getSetting('nitro_emulator_service', 'emulator'),
|
||||||
'auto_update_days' => $this->getSetting('auto_update_days', '0,6'),
|
'nitro_db_name' => $this->getSetting('nitro_db_name', 'habbo'),
|
||||||
'nitro_client_path' => $this->getSetting('nitro_client_path', $paths['nitro_client_path']),
|
'nitro_sql_dir' => $this->getSetting('nitro_sql_dir', '/var/www/emulator/Database Updates'),
|
||||||
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', $paths['nitro_renderer_path']),
|
'nitro_backup_dir' => $this->getSetting('nitro_backup_dir', '/var/www/emulator/Database Updates/backups'),
|
||||||
'nitro_build_path' => $this->getSetting('nitro_build_path', $paths['nitro_build_path']),
|
'nitro_gamedata_dir' => $this->getSetting('nitro_gamedata_dir', '/var/www/Gamedata/config'),
|
||||||
'nitro_webroot' => $this->getSetting('nitro_webroot', $paths['nitro_webroot']),
|
'nitro_client_dir' => $this->getSetting('nitro_client_dir', '/var/www/Nitro-V3/public/configuration'),
|
||||||
'gamedata_path' => $this->getSetting('gamedata_path', $paths['gamedata_path']),
|
'nitro_client_src' => $this->getSetting('nitro_client_src', '/var/www/Nitro-V3'),
|
||||||
'nitro_github_branch' => $this->getSetting('nitro_github_branch', 'main'),
|
'nitro_renderer_src' => $this->getSetting('nitro_renderer_src', '/var/www/Nitro_Render_V3'),
|
||||||
'nitro_github_url' => $this->getSetting('nitro_github_url', ''),
|
|
||||||
'nitro_site_url' => $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()),
|
|
||||||
'nitro_auto_update_configs' => $this->getSettingBool('nitro_auto_update_configs'),
|
|
||||||
'nitro_auto_update_enabled' => $this->getSettingBool('nitro_auto_update_enabled'),
|
|
||||||
'nitro_auto_update_schedule' => $this->getSetting('nitro_auto_update_schedule', '03:00'),
|
|
||||||
'nitro_auto_update_days' => $this->getSetting('nitro_auto_update_days', '0,6'),
|
|
||||||
'hotel_alert_message' => '',
|
'hotel_alert_message' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -233,90 +225,13 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
->content(fn () => $this->renderEmulatorInfoView()),
|
->content(fn () => $this->renderEmulatorInfoView()),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Section::make(__('commandocentrum.emulator_updates'))
|
Section::make(__('commandocentrum.nitro_update'))
|
||||||
->description(__('commandocentrum.emulator_updates_desc'))
|
->description(__('commandocentrum.nitro_update_desc'))
|
||||||
->icon('heroicon-o-arrow-down-circle')
|
->icon('heroicon-o-arrow-path')
|
||||||
->afterHeader([
|
|
||||||
Action::make('check_updates')
|
|
||||||
->label(__('commandocentrum.check_updates'))
|
|
||||||
->color('info')
|
|
||||||
->action('checkEmulatorUpdates'),
|
|
||||||
Action::make('build_emulator')
|
|
||||||
->label('🔨 ' . __('commandocentrum.build'))
|
|
||||||
->color('success')
|
|
||||||
->action('buildEmulator'),
|
|
||||||
Action::make('run_sql')
|
|
||||||
->label(__('commandocentrum.sql_updates'))
|
|
||||||
->color('purple')
|
|
||||||
->action('runSqlUpdates'),
|
|
||||||
Action::make('save_emulator')
|
|
||||||
->label(__('commandocentrum.save'))
|
|
||||||
->color('primary')
|
|
||||||
->action('saveEmulator'),
|
|
||||||
])
|
|
||||||
->schema([
|
->schema([
|
||||||
Placeholder::make('emulator_settings')
|
Placeholder::make('nitro_cli_only')
|
||||||
->label('')
|
->label('')
|
||||||
->content(fn () => $this->renderEmulatorSettingsView()),
|
->content(__('commandocentrum.nitro_cli_only')),
|
||||||
]),
|
|
||||||
|
|
||||||
Section::make(__('commandocentrum.emulator_backups'))
|
|
||||||
->description(__('commandocentrum.emulator_backups_desc'))
|
|
||||||
->icon('heroicon-s-archive-box')
|
|
||||||
->schema([
|
|
||||||
Placeholder::make('backups_list')
|
|
||||||
->label('')
|
|
||||||
->content(fn () => $this->renderBackupsListView()),
|
|
||||||
]),
|
|
||||||
|
|
||||||
Section::make(__('commandocentrum.nitro_client'))
|
|
||||||
->description(__('commandocentrum.nitro_client_desc'))
|
|
||||||
->icon('heroicon-o-cloud-arrow-down')
|
|
||||||
->afterHeader([
|
|
||||||
Action::make('detect_paths')
|
|
||||||
->label('🔍 ' . __('commandocentrum.auto_detect'))
|
|
||||||
->color('success')
|
|
||||||
->action('detectAndSavePaths'),
|
|
||||||
Action::make('check_nitro')
|
|
||||||
->label(__('commandocentrum.check'))
|
|
||||||
->color('info')
|
|
||||||
->action('checkNitroUpdates'),
|
|
||||||
Action::make('build_nitro')
|
|
||||||
->label(__('commandocentrum.build'))
|
|
||||||
->color('pink')
|
|
||||||
->action('buildNitro'),
|
|
||||||
Action::make('generate_configs')
|
|
||||||
->label(__('commandocentrum.generate_configs'))
|
|
||||||
->color('indigo')
|
|
||||||
->action('generateNitroConfigs'),
|
|
||||||
Action::make('save_nitro')
|
|
||||||
->label(__('commandocentrum.save'))
|
|
||||||
->color('primary')
|
|
||||||
->action('saveNitro'),
|
|
||||||
])
|
|
||||||
->schema([
|
|
||||||
Placeholder::make('nitro_settings')
|
|
||||||
->label('')
|
|
||||||
->content(fn () => $this->renderNitroSettingsView()),
|
|
||||||
]),
|
|
||||||
|
|
||||||
Section::make(__('commandocentrum.auto_updates'))
|
|
||||||
->description(__('commandocentrum.auto_updates_desc'))
|
|
||||||
->icon('heroicon-o-clock')
|
|
||||||
->columns(2)
|
|
||||||
->afterHeader([
|
|
||||||
Action::make('save_auto')
|
|
||||||
->label(__('commandocentrum.save'))
|
|
||||||
->color('primary')
|
|
||||||
->action('saveAutoUpdate'),
|
|
||||||
])
|
|
||||||
->schema([
|
|
||||||
Toggle::make('auto_update_enabled')
|
|
||||||
->label(__('commandocentrum.enable_auto_updates')),
|
|
||||||
TextInput::make('auto_update_schedule')
|
|
||||||
->label(__('commandocentrum.schedule')),
|
|
||||||
TextInput::make('auto_update_days')
|
|
||||||
->label(__('commandocentrum.days')),
|
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Section::make(__('commandocentrum.clothing_sync'))
|
Section::make(__('commandocentrum.clothing_sync'))
|
||||||
@@ -367,15 +282,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
->helperText(__('commandocentrum.discord_ranks_helper')),
|
->helperText(__('commandocentrum.discord_ranks_helper')),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Section::make(__('commandocentrum.update_history'))
|
|
||||||
->description(__('commandocentrum.update_history_desc'))
|
|
||||||
->icon('heroicon-o-clock')
|
|
||||||
->schema([
|
|
||||||
Placeholder::make('history')
|
|
||||||
->label('')
|
|
||||||
->content(fn () => $this->renderUpdateHistoryView()),
|
|
||||||
]),
|
|
||||||
|
|
||||||
Section::make(__('commandocentrum.social_login'))
|
Section::make(__('commandocentrum.social_login'))
|
||||||
->description(__('commandocentrum.social_login_desc'))
|
->description(__('commandocentrum.social_login_desc'))
|
||||||
->icon('heroicon-o-user-circle')
|
->icon('heroicon-o-user-circle')
|
||||||
@@ -491,100 +397,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderEmulatorSettingsView(): View
|
|
||||||
{
|
|
||||||
return view('filament.components.commandocentrum.emulator-settings', [
|
|
||||||
'emulatorBranchesHtml' => $this->getEmulatorBranchesHtml(),
|
|
||||||
'emulatorStatusHtml' => $this->renderEmulatorStatusView()->render(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderEmulatorStatusView(): View
|
|
||||||
{
|
|
||||||
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
|
|
||||||
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
|
|
||||||
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source');
|
|
||||||
$githubUrl = $this->getSetting('emulator_github_url', '');
|
|
||||||
$branch = $this->getSetting('emulator_github_branch', 'main');
|
|
||||||
|
|
||||||
$jarExists = $this->fileExists($jarPath);
|
|
||||||
$sourceExists = $this->fileExists($sourcePath);
|
|
||||||
$sourceCommit = $this->getGitCommit($sourcePath);
|
|
||||||
$remoteVersion = $githubUrl !== '' && $githubUrl !== '0' ? $this->getRemoteCommit($githubUrl, $branch) : 'N/A';
|
|
||||||
|
|
||||||
$canBuild = false;
|
|
||||||
$checkDirs = [
|
|
||||||
$sourcePath,
|
|
||||||
$sourcePath . '/Emulator',
|
|
||||||
$sourcePath . '/Emulator/Emulator',
|
|
||||||
$sourcePath . '/emulator',
|
|
||||||
$sourcePath . '/emulator/emulator',
|
|
||||||
];
|
|
||||||
foreach ($checkDirs as $dir) {
|
|
||||||
$check = $this->runCommand('test -f ' . escapeshellarg($dir . '/pom.xml') . ' && echo yes');
|
|
||||||
if ($check && trim($check) === 'yes') {
|
|
||||||
$canBuild = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('filament.components.commandocentrum.emulator-status', [
|
|
||||||
'emulatorOnline' => $this->getEmulatorStatusText() === 'Online',
|
|
||||||
'jarExists' => $jarExists,
|
|
||||||
'serviceName' => $serviceName,
|
|
||||||
'sourceCommit' => $sourceCommit,
|
|
||||||
'remoteVersion' => $remoteVersion,
|
|
||||||
'canBuild' => $canBuild,
|
|
||||||
'jarPath' => $jarPath,
|
|
||||||
'sourcePath' => $sourcePath,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderNitroSettingsView(): View
|
|
||||||
{
|
|
||||||
return view('filament.components.commandocentrum.nitro-settings', [
|
|
||||||
'nitroBranchesHtml' => $this->getNitroBranchesHtml(),
|
|
||||||
'nitroStatusHtml' => $this->renderNitroStatusView()->render(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderNitroStatusView(): View
|
|
||||||
{
|
|
||||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
|
||||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
|
||||||
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
|
|
||||||
$clientGithubUrl = $this->getSetting('nitro_github_url', '');
|
|
||||||
$rendererGithubUrl = $this->getSetting('nitro_renderer_github_url', 'https://github.com/duckietm/Nitro_Render_V3');
|
|
||||||
|
|
||||||
$clientCommit = $this->getGitCommit($clientPath);
|
|
||||||
$rendererCommit = $this->getGitCommit($rendererPath);
|
|
||||||
$clientRemote = $clientGithubUrl !== '' && $clientGithubUrl !== '0' ? $this->getRemoteCommit($clientGithubUrl, $this->getSetting('nitro_github_branch', 'main')) : 'N/A';
|
|
||||||
$rendererRemote = $rendererGithubUrl !== '' && $rendererGithubUrl !== '0' ? $this->getRemoteCommit($rendererGithubUrl, $this->getSetting('nitro_renderer_github_branch', 'main')) : 'N/A';
|
|
||||||
|
|
||||||
return view('filament.components.commandocentrum.nitro-status', [
|
|
||||||
'clientExists' => $this->checkPathExists($clientPath),
|
|
||||||
'rendererExists' => $this->checkPathExists($rendererPath),
|
|
||||||
'webrootExists' => $this->checkPathExists($webroot),
|
|
||||||
'clientCommit' => $clientCommit,
|
|
||||||
'rendererCommit' => $rendererCommit,
|
|
||||||
'clientRemote' => $clientRemote,
|
|
||||||
'rendererRemote' => $rendererRemote,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderBackupsListView(): View
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$backups = app(EmulatorControlAction::class)->getBackups();
|
|
||||||
} catch (Exception) {
|
|
||||||
$backups = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('filament.components.commandocentrum.backups-list', [
|
|
||||||
'backups' => $backups,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function renderClothingStatusView(): View
|
private function renderClothingStatusView(): View
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -614,19 +426,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderUpdateHistoryView(): View
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$history = app(UpdateHistoryService::class)->getRecent(10);
|
|
||||||
} catch (Exception) {
|
|
||||||
$history = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('filament.components.commandocentrum.update-history', [
|
|
||||||
'history' => $history,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getSetting(string $key, string $default = ''): string
|
private function getSetting(string $key, string $default = ''): string
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -878,33 +677,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function checkEmulatorUpdates(): void
|
|
||||||
{
|
|
||||||
$result = app(EmulatorControlAction::class)->update();
|
|
||||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
$this->fillForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildEmulator(): void
|
|
||||||
{
|
|
||||||
$result = app(EmulatorControlAction::class)->build();
|
|
||||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function runSqlUpdates(): void
|
|
||||||
{
|
|
||||||
$result = app(EmulatorControlAction::class)->runSqlUpdates();
|
|
||||||
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function restoreBackup(string $backupName): void
|
|
||||||
{
|
|
||||||
$result = app(EmulatorControlAction::class)->restoreBackup($backupName);
|
|
||||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), $result['success'] ? 'success' : 'danger');
|
|
||||||
$this->fillForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveEmulator(): void
|
public function saveEmulator(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -924,38 +696,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function checkNitroUpdates(): void
|
|
||||||
{
|
|
||||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
|
||||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
|
||||||
$branch = $this->getSetting('nitro_github_branch', 'main');
|
|
||||||
|
|
||||||
$result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
|
|
||||||
$this->notify(__('commandocentrum.success'), $result['message'], 'success');
|
|
||||||
$this->fillForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildNitro(): void
|
|
||||||
{
|
|
||||||
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
|
|
||||||
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
|
|
||||||
$branch = $this->getSetting('nitro_github_branch', 'main');
|
|
||||||
|
|
||||||
$result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
|
|
||||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.warning'), $result['message'], $result['success'] ? 'success' : 'warning');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateNitroConfigs(): void
|
|
||||||
{
|
|
||||||
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
|
|
||||||
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
|
|
||||||
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
|
|
||||||
|
|
||||||
$result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath);
|
|
||||||
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger');
|
|
||||||
$this->fillForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function syncClothing(): void
|
public function syncClothing(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -972,58 +712,6 @@ final class Commandocentrum extends Page implements HasForms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function detectAndSavePaths(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$paths = $this->autoDetectPaths();
|
|
||||||
$settings = app(SettingsService::class);
|
|
||||||
|
|
||||||
$settings->set('nitro_client_path', $paths['nitro_client_path']);
|
|
||||||
$settings->set('nitro_renderer_path', $paths['nitro_renderer_path']);
|
|
||||||
$settings->set('nitro_build_path', $paths['nitro_build_path']);
|
|
||||||
$settings->set('nitro_webroot', $paths['nitro_webroot']);
|
|
||||||
$settings->set('gamedata_path', $paths['gamedata_path']);
|
|
||||||
$settings->set('emulator_jar_path', $paths['emulator_jar_path']);
|
|
||||||
$settings->set('emulator_source_path', $paths['emulator_source_path']);
|
|
||||||
|
|
||||||
$this->fillForm();
|
|
||||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.paths_detected'), 'success');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveNitro(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$settings = app(SettingsService::class);
|
|
||||||
$settings->set('nitro_client_path', $this->data['nitro_client_path'] ?? '/var/www/atomcms/nitro-client');
|
|
||||||
$settings->set('nitro_renderer_path', $this->data['nitro_renderer_path'] ?? '/var/www/atomcms/nitro-renderer');
|
|
||||||
$settings->set('nitro_build_path', $this->data['nitro_build_path'] ?? '/var/www/atomcms/nitro-client/dist');
|
|
||||||
$settings->set('nitro_webroot', $this->data['nitro_webroot'] ?? '/var/www/Client');
|
|
||||||
$settings->set('gamedata_path', $this->data['gamedata_path'] ?? '/var/www/Gamedata');
|
|
||||||
$settings->set('nitro_github_url', $this->data['nitro_github_url'] ?? '');
|
|
||||||
$settings->set('nitro_github_branch', $this->data['nitro_github_branch'] ?? 'main');
|
|
||||||
$settings->set('nitro_site_url', $this->data['nitro_site_url'] ?? '');
|
|
||||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.nitro_settings_saved'), 'success');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveAutoUpdate(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$settings = app(SettingsService::class);
|
|
||||||
$settings->set('auto_update_enabled', ($this->data['auto_update_enabled'] ?? false) ? '1' : '0');
|
|
||||||
$settings->set('auto_update_schedule', $this->data['auto_update_schedule'] ?? '03:00');
|
|
||||||
$settings->set('auto_update_days', $this->data['auto_update_days'] ?? '0,6');
|
|
||||||
$this->notify(__('commandocentrum.success'), __('commandocentrum.auto_update_saved'), 'success');
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function saveAlerts(): void
|
public function saveAlerts(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Filament\Pages\Radio;
|
|||||||
|
|
||||||
use App\Models\RadioApiKey;
|
use App\Models\RadioApiKey;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
@@ -82,23 +81,19 @@ final class ApiKeys extends Page implements HasTable
|
|||||||
->label('Aangemaakt')
|
->label('Aangemaakt')
|
||||||
->dateTime('d-m-Y H:i'),
|
->dateTime('d-m-Y H:i'),
|
||||||
])
|
])
|
||||||
->recordActions(function ($record) {
|
->actions([
|
||||||
return [
|
Action::make('toggle')
|
||||||
ActionGroup::make([
|
->label(fn ($record) => $record->is_active ? 'Deactiveren' : 'Activeren')
|
||||||
Action::make('toggle')
|
->icon(fn ($record) => $record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||||
->label($record->is_active ? 'Deactiveren' : 'Activeren')
|
->action(fn ($record) => $this->toggleKey($record)),
|
||||||
->icon($record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
Action::make('delete')
|
||||||
->action(fn () => $this->toggleKey($record)),
|
->label('Verwijderen')
|
||||||
Action::make('delete')
|
->icon('heroicon-o-trash')
|
||||||
->label('Verwijderen')
|
->color('danger')
|
||||||
->icon('heroicon-o-trash')
|
->requiresConfirmation()
|
||||||
->color('danger')
|
->action(fn ($record) => $record->delete()),
|
||||||
->requiresConfirmation()
|
])
|
||||||
->action(fn () => $record->delete()),
|
->headerActions([
|
||||||
]),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->toolbarActions([
|
|
||||||
Action::make('create')
|
Action::make('create')
|
||||||
->label('Nieuwe API Sleutel')
|
->label('Nieuwe API Sleutel')
|
||||||
->icon('heroicon-o-plus')
|
->icon('heroicon-o-plus')
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Filament\Pages\Radio;
|
|||||||
|
|
||||||
use App\Models\RadioAutoDjTrack;
|
use App\Models\RadioAutoDjTrack;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
@@ -74,31 +73,27 @@ final class AutoDjPlaylist extends Page implements HasTable
|
|||||||
->trueColor('success')
|
->trueColor('success')
|
||||||
->falseColor('danger'),
|
->falseColor('danger'),
|
||||||
])
|
])
|
||||||
->recordActions(function ($record) {
|
->actions([
|
||||||
return [
|
Action::make('toggle_active')
|
||||||
ActionGroup::make([
|
->label(fn ($record) => $record->is_active ? 'Deactiveren' : 'Activeren')
|
||||||
Action::make('toggle_active')
|
->icon(fn ($record) => $record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
||||||
->label($record->is_active ? 'Deactiveren' : 'Activeren')
|
->action(fn ($record) => $this->toggleActive($record)),
|
||||||
->icon($record->is_active ? 'heroicon-o-pause' : 'heroicon-o-play')
|
Action::make('move_up')
|
||||||
->action(fn () => $this->toggleActive($record)),
|
->label('Omhoog')
|
||||||
Action::make('move_up')
|
->icon('heroicon-o-chevron-up')
|
||||||
->label('Omhoog')
|
->action(fn ($record) => $this->moveUp($record)),
|
||||||
->icon('heroicon-o-chevron-up')
|
Action::make('move_down')
|
||||||
->action(fn () => $this->moveUp($record)),
|
->label('Omlaag')
|
||||||
Action::make('move_down')
|
->icon('heroicon-o-chevron-down')
|
||||||
->label('Omlaag')
|
->action(fn ($record) => $this->moveDown($record)),
|
||||||
->icon('heroicon-o-chevron-down')
|
Action::make('delete')
|
||||||
->action(fn () => $this->moveDown($record)),
|
->label('Verwijderen')
|
||||||
Action::make('delete')
|
->icon('heroicon-o-trash')
|
||||||
->label('Verwijderen')
|
->color('danger')
|
||||||
->icon('heroicon-o-trash')
|
->requiresConfirmation()
|
||||||
->color('danger')
|
->action(fn ($record) => $record->delete()),
|
||||||
->requiresConfirmation()
|
])
|
||||||
->action(fn () => $record->delete()),
|
->headerActions([
|
||||||
]),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->toolbarActions([
|
|
||||||
Action::make('create')
|
Action::make('create')
|
||||||
->label('Track Toevoegen')
|
->label('Track Toevoegen')
|
||||||
->icon('heroicon-o-plus')
|
->icon('heroicon-o-plus')
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ final class EmbedCode extends Page implements HasForms
|
|||||||
public function form(Schema $schema): Schema
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
|
->statePath('data')
|
||||||
->components([
|
->components([
|
||||||
Section::make('Embed Configuratie')
|
Section::make('Embed Configuratie')
|
||||||
->description('Pas het uiterlijk van de embed player aan')
|
->description('Pas het uiterlijk van de embed player aan')
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ final class PointsSettings extends Page implements HasForms
|
|||||||
|
|
||||||
public function resetLeaderboard(): void
|
public function resetLeaderboard(): void
|
||||||
{
|
{
|
||||||
User::where('radio_points', '>', 0)->update(['radio_points' => 0]);
|
User::query()->where('radio_points', '>', 0)->each(fn (User $u) => $u->forceFill(['radio_points' => 0])->save());
|
||||||
RadioListenerPoint::query()->delete();
|
RadioListenerPoint::query()->delete();
|
||||||
|
|
||||||
$this->pointsService->clearLeaderboardCache();
|
$this->pointsService->clearLeaderboardCache();
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class StaffApplicationResource extends Resource
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((int) $user->team_id !== (int) $team->id) {
|
if ((int) $user->team_id !== (int) $team->id) {
|
||||||
$user->update(['team_id' => $team->id]);
|
$user->forceFill(['team_id' => $team->id])->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
$r->update([
|
$r->update([
|
||||||
@@ -177,7 +177,7 @@ class StaffApplicationResource extends Resource
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($r->status === 'approved' && (int) $user->team_id === (int) $team->id) {
|
if ($r->status === 'approved' && (int) $user->team_id === (int) $team->id) {
|
||||||
$user->update(['team_id' => null]);
|
$user->forceFill(['team_id' => null])->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
$r->update([
|
$r->update([
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\Miscellaneous\AlertLogResource;
|
namespace App\Filament\Resources\Miscellaneous\AlertLogResource;
|
||||||
|
|
||||||
use App\Models\Miscellaneous\AlertLog;
|
use App\Models\Miscellaneous\AlertLog;
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
@@ -136,14 +135,12 @@ class AlertLogResource extends Resource
|
|||||||
->icon('heroicon-o-trash')
|
->icon('heroicon-o-trash')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->action(function () {
|
->action(function () {
|
||||||
$updateService = new EmulatorUpdateService;
|
|
||||||
$result = $updateService->clearAllLogs();
|
|
||||||
Cache::flush();
|
Cache::flush();
|
||||||
AlertLog::truncate();
|
AlertLog::truncate();
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->success()
|
->success()
|
||||||
->title('🗑️ Alle Logs Geleegd!')
|
->title('🗑️ Alle Logs Geleegd!')
|
||||||
->body($result['message'])
|
->body('Alle logs zijn gewist.')
|
||||||
->send();
|
->send();
|
||||||
})
|
})
|
||||||
->requiresConfirmation(),
|
->requiresConfirmation(),
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class RadioApplicationResource extends Resource
|
|||||||
])
|
])
|
||||||
->label('Status'),
|
->label('Status'),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('approve')
|
Action::make('approve')
|
||||||
->label('Goedkeuren')
|
->label('Goedkeuren')
|
||||||
->icon('heroicon-o-check-circle')
|
->icon('heroicon-o-check-circle')
|
||||||
|
|||||||
@@ -123,11 +123,11 @@ class RadioBannerResource extends Resource
|
|||||||
->label('Actief')
|
->label('Actief')
|
||||||
->boolean(),
|
->boolean(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->headerActions([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,11 @@ class RadioHistoryResource extends Resource
|
|||||||
TextColumn::make('listeners_count')
|
TextColumn::make('listeners_count')
|
||||||
->label('Luisteraars'),
|
->label('Luisteraars'),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->headerActions([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ class RadioRankResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->headerActions([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,11 @@ class RadioScheduleResource extends Resource
|
|||||||
])
|
])
|
||||||
->label('Dag'),
|
->label('Dag'),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->toolbarActions([
|
->headerActions([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class RadioShoutResource extends Resource
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
Action::make('report')
|
Action::make('report')
|
||||||
->label('Melden')
|
->label('Melden')
|
||||||
->icon('heroicon-o-flag')
|
->icon('heroicon-o-flag')
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class RadioSongPlayResource extends Resource
|
|||||||
->options(fn () => RadioSongPlay::distinct()->whereNotNull('artist')->pluck('artist', 'artist')->toArray())
|
->options(fn () => RadioSongPlay::distinct()->whereNotNull('artist')->pluck('artist', 'artist')->toArray())
|
||||||
->searchable(),
|
->searchable(),
|
||||||
])
|
])
|
||||||
->recordActions([
|
->actions([
|
||||||
DeleteAction::make()
|
DeleteAction::make()
|
||||||
->label('Verwijderen'),
|
->label('Verwijderen'),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class EditUser extends EditRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->online) {
|
if (! $user->online) {
|
||||||
$user->update(['rank' => $data['rank']]);
|
$user->forceFill(['rank' => $data['rank']])->save();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Widgets;
|
|
||||||
|
|
||||||
use App\Services\EmulatorUpdateService;
|
|
||||||
use App\Services\NitroUpdateService;
|
|
||||||
use App\Services\RconService;
|
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Widgets\Widget;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class UpdateCheckerWidget extends Widget
|
|
||||||
{
|
|
||||||
#[\Override]
|
|
||||||
protected string $view = 'filament.widgets.update-checker';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected int|string|array $columnSpan = 'full';
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
protected static ?int $sort = 0;
|
|
||||||
|
|
||||||
public ?string $emulatorVersion = null;
|
|
||||||
|
|
||||||
public ?string $latestEmulatorVersion = null;
|
|
||||||
|
|
||||||
public bool $emulatorUpdate = false;
|
|
||||||
|
|
||||||
public ?string $nitroVersion = null;
|
|
||||||
|
|
||||||
public ?string $latestNitroVersion = null;
|
|
||||||
|
|
||||||
public bool $nitroUpdate = false;
|
|
||||||
|
|
||||||
public int $onlineUsers = 0;
|
|
||||||
|
|
||||||
public string $dbSize = '0 MB';
|
|
||||||
|
|
||||||
public bool $hasAnyUpdate = false;
|
|
||||||
|
|
||||||
public int $sqlApplied = 0;
|
|
||||||
|
|
||||||
public int $sqlPending = 0;
|
|
||||||
|
|
||||||
public bool $sqlTableExists = false;
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$this->emulatorVersion = setting('emulator_version', '?');
|
|
||||||
$this->nitroVersion = setting('nitro_client_version', '?');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->onlineUsers = (int) DB::connection('mysql')->table('users')->where('online', '1')->count();
|
|
||||||
$sizeResult = DB::connection('mysql')->select('SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) as db_size FROM information_schema.tables WHERE table_schema = DATABASE()');
|
|
||||||
$this->dbSize = ($sizeResult[0]->db_size ?? '0') . ' MB';
|
|
||||||
} catch (\Exception) {
|
|
||||||
$this->dbSize = '? MB';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->loadSqlStatus();
|
|
||||||
|
|
||||||
$cached = Cache::get('all_updates_check');
|
|
||||||
|
|
||||||
if ($cached !== null) {
|
|
||||||
$this->emulatorUpdate = $cached['emulator'] ?? false;
|
|
||||||
$this->latestEmulatorVersion = $cached['emulator_version'] ?? null;
|
|
||||||
$this->nitroUpdate = $cached['nitro'] ?? false;
|
|
||||||
$this->latestNitroVersion = $cached['nitro_version'] ?? null;
|
|
||||||
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->performCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadSqlStatus(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$updateService = new EmulatorUpdateService;
|
|
||||||
$sqlDiagnosis = $updateService->diagnoseSqlUpdates();
|
|
||||||
|
|
||||||
$this->sqlTableExists = $sqlDiagnosis['table_exists'] ?? false;
|
|
||||||
$this->sqlApplied = $sqlDiagnosis['applied_count'] ?? 0;
|
|
||||||
$this->sqlPending = $sqlDiagnosis['pending_count'] ?? 0;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[UpdateChecker] SQL status failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function performCheck(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$updateService = new EmulatorUpdateService;
|
|
||||||
$check = $updateService->checkForUpdates();
|
|
||||||
$this->emulatorUpdate = $check['update_available'] ?? false;
|
|
||||||
$this->latestEmulatorVersion = $check['latest_version'] ?? null;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[UpdateChecker] Emulator check failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
$nitroCheck = $nitroService->checkForUpdates();
|
|
||||||
$this->nitroUpdate = $nitroCheck['has_updates'] ?? false;
|
|
||||||
if ($this->nitroUpdate) {
|
|
||||||
$parts = [];
|
|
||||||
if ($nitroCheck['client_update'] ?? false) {
|
|
||||||
$parts[] = 'Client';
|
|
||||||
}
|
|
||||||
if ($nitroCheck['renderer_update'] ?? false) {
|
|
||||||
$parts[] = 'Renderer';
|
|
||||||
}
|
|
||||||
$this->latestNitroVersion = implode(' + ', $parts);
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[UpdateChecker] Nitro check failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
|
|
||||||
|
|
||||||
Cache::put('all_updates_check', [
|
|
||||||
'emulator' => $this->emulatorUpdate,
|
|
||||||
'emulator_version' => $this->latestEmulatorVersion,
|
|
||||||
'nitro' => $this->nitroUpdate,
|
|
||||||
'nitro_version' => $this->latestNitroVersion,
|
|
||||||
], now()->addMinutes(15));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function forceCheck(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
$updateService = new EmulatorUpdateService;
|
|
||||||
$updateService->resetInstalledDate();
|
|
||||||
$this->performCheck();
|
|
||||||
|
|
||||||
if ($this->hasAnyUpdate) {
|
|
||||||
Notification::make()->success()->title('Updates Gevonden!')->body('Klik op "Alles Updaten" om te installeren')->send();
|
|
||||||
} else {
|
|
||||||
Notification::make()->info()->title('Geen Updates')->body('Alles is al up-to-date')->send();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function repairSystem(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
|
|
||||||
$messages = [];
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$emuService = new EmulatorUpdateService;
|
|
||||||
$repairResult = $emuService->repairEmulator();
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$messages[] = '🖥️ Emulator: ' . ($repairResult['message'] ?? 'Gerepareerd');
|
|
||||||
} else {
|
|
||||||
$errors[] = 'Emulator: ' . ($repairResult['error'] ?? 'Onbekende fout');
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'Emulator: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
$repairResult = $nitroService->repair();
|
|
||||||
|
|
||||||
if ($repairResult['success']) {
|
|
||||||
$messages[] = '🎮 Nitro: ' . ($repairResult['message'] ?? 'Gerepareerd');
|
|
||||||
} else {
|
|
||||||
$errors[] = 'Nitro: ' . ($repairResult['error'] ?? 'Onbekende fout');
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'Nitro: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($messages !== []) {
|
|
||||||
Notification::make()->success()->title('Reparatie Voltooid!')->body(implode(' | ', $messages))->send();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($errors !== []) {
|
|
||||||
Notification::make()->danger()->title('Reparatie Fout')->body(implode(' | ', $errors))->send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
$this->mount();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function diagnoseSystem(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$emuService = new EmulatorUpdateService;
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
|
|
||||||
$emuDiagnosis = $emuService->diagnose();
|
|
||||||
$nitroDiagnosis = $nitroService->diagnose();
|
|
||||||
|
|
||||||
$issues = array_merge(
|
|
||||||
$emuDiagnosis['issues'] ?? [],
|
|
||||||
$nitroDiagnosis['issues'] ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
$recommendations = array_merge(
|
|
||||||
$emuDiagnosis['recommendations'] ?? [],
|
|
||||||
$nitroDiagnosis['recommendations'] ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($issues === []) {
|
|
||||||
Notification::make()->info()->title('Diagnose')->body('Geen problemen gevonden')->send();
|
|
||||||
} else {
|
|
||||||
$body = 'Problemen: ' . implode(', ', $issues);
|
|
||||||
if ($recommendations !== []) {
|
|
||||||
$body .= "\n\nAanbevelingen: " . implode(', ', $recommendations);
|
|
||||||
}
|
|
||||||
Notification::make()->warning()->title('Diagnose Resultaat')->body($body)->send();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateAll(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
Cache::forget('website_settings');
|
|
||||||
|
|
||||||
$messages = [];
|
|
||||||
$errors = [];
|
|
||||||
$updateService = new EmulatorUpdateService;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = $updateService->updateEmulator();
|
|
||||||
if ($result['success']) {
|
|
||||||
$messages[] = '🖥️ ' . ($result['message'] ?? 'Emulator geüpdatet');
|
|
||||||
} else {
|
|
||||||
$error = $result['error'] ?? '';
|
|
||||||
if (str_contains($error, 'al up-to-date')) {
|
|
||||||
$messages[] = '🖥️ Emulator al up-to-date';
|
|
||||||
} else {
|
|
||||||
$errors[] = 'Emulator: ' . $error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'Emulator: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$nitroService = new NitroUpdateService;
|
|
||||||
$nitroCheck = $nitroService->checkForUpdates();
|
|
||||||
|
|
||||||
if ($nitroCheck['has_updates'] ?? false) {
|
|
||||||
$result = $nitroService->updateNitro();
|
|
||||||
if ($result['success']) {
|
|
||||||
$parts = [];
|
|
||||||
if ($result['renderer_updated'] ?? false) {
|
|
||||||
$parts[] = 'Renderer';
|
|
||||||
}
|
|
||||||
if ($result['client_updated'] ?? false) {
|
|
||||||
$parts[] = 'Client';
|
|
||||||
}
|
|
||||||
if ($result['built'] ?? false) {
|
|
||||||
$parts[] = 'Build';
|
|
||||||
}
|
|
||||||
if ($result['deployed'] ?? false) {
|
|
||||||
$parts[] = 'Deploy';
|
|
||||||
}
|
|
||||||
if ($parts !== []) {
|
|
||||||
$messages[] = '🎮 Nitro: ' . implode(', ', $parts);
|
|
||||||
}
|
|
||||||
} elseif (! empty($result['errors'] ?? [])) {
|
|
||||||
$errors = array_merge($errors, $result['errors']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$messages[] = '🎮 Nitro al up-to-date';
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'Nitro: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sqlResult = $updateService->runSqlUpdates();
|
|
||||||
if ($sqlResult['sql_updated'] ?? false) {
|
|
||||||
$count = count($sqlResult['files_run'] ?? []);
|
|
||||||
if ($count > 0) {
|
|
||||||
$messages[] = "📊 {$count} SQL updates";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'SQL: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$updateService->restartEmulator();
|
|
||||||
$messages[] = '🔄 Emulator herstart';
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[UpdateChecker] Restart failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$rcon = new RconService;
|
|
||||||
$rcon->sendCommand('updatecatalog');
|
|
||||||
$rcon->sendCommand('updatewordfilter');
|
|
||||||
$messages[] = '📚 Catalogus + filter vernieuwd';
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[UpdateChecker] RCON failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($messages !== []) {
|
|
||||||
Notification::make()->success()->title('Updates Voltooid!')->body(implode(' | ', $messages))->send();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($errors !== []) {
|
|
||||||
Notification::make()->danger()->title('Sommige Updates Mislukt')->body(implode(' | ', $errors))->send();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($messages === [] && $errors === []) {
|
|
||||||
Notification::make()->info()->title('Geen Updates')->body('Alles was al up-to-date')->send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::forget('all_updates_check');
|
|
||||||
$this->mount();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Override]
|
|
||||||
public static function canView(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Api\ArticleResource;
|
||||||
|
use App\Models\Articles\WebsiteArticle;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class ArticleApiController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$articles = WebsiteArticle::with(['user:id,username,look'])
|
||||||
|
->latest('id')
|
||||||
|
->paginate(12);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => ArticleResource::collection($articles),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $articles->currentPage(),
|
||||||
|
'last_page' => $articles->lastPage(),
|
||||||
|
'per_page' => $articles->perPage(),
|
||||||
|
'total' => $articles->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $slug): JsonResponse
|
||||||
|
{
|
||||||
|
$article = WebsiteArticle::with(['user:id,username,look', 'comments.user:id,username,look'])
|
||||||
|
->where('slug', $slug)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return response()->json(['data' => new ArticleResource($article)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Game\Furniture\CatalogItem;
|
||||||
|
use App\Models\Game\Furniture\CatalogPage;
|
||||||
|
use App\Models\Game\Guild\Guild;
|
||||||
|
use App\Models\Miscellaneous\WebsiteSetting;
|
||||||
|
use App\Services\Community\StaffService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class ContentApiController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly StaffService $staffService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function settings(): JsonResponse
|
||||||
|
{
|
||||||
|
$settings = Cache::remember('api_all_settings', 60, fn () => WebsiteSetting::all()->pluck('value', 'key'));
|
||||||
|
|
||||||
|
return response()->json(['data' => $settings]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function staff(): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json(['data' => $this->staffService->fetchStaffPositions()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function teams(): JsonResponse
|
||||||
|
{
|
||||||
|
$teams = Guild::with('members')->latest('id')->paginate(12);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $teams->items(),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $teams->currentPage(),
|
||||||
|
'last_page' => $teams->lastPage(),
|
||||||
|
'per_page' => $teams->perPage(),
|
||||||
|
'total' => $teams->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rareValues(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$query = CatalogItem::query();
|
||||||
|
|
||||||
|
if ($categoryId = $request->query('category')) {
|
||||||
|
$query->where('page_id', $categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $query->with('itemBase')->latest('id')->paginate(24);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $items->items(),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $items->currentPage(),
|
||||||
|
'last_page' => $items->lastPage(),
|
||||||
|
'per_page' => $items->perPage(),
|
||||||
|
'total' => $items->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rareValuesCategories(): JsonResponse
|
||||||
|
{
|
||||||
|
$categories = CatalogPage::where('catalog_name', '!=', '')
|
||||||
|
->where('visible', 1)
|
||||||
|
->orderBy('order_number')
|
||||||
|
->get(['id', 'catalog_name', 'icon']);
|
||||||
|
|
||||||
|
return response()->json(['data' => $categories]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Game\Furniture\CatalogItem;
|
||||||
|
use App\Models\Game\Furniture\CatalogPage;
|
||||||
|
use App\Models\Game\Furniture\ItemBase;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class FurniEditorController extends Controller
|
class FurniEditorController extends Controller
|
||||||
@@ -25,10 +29,10 @@ class FurniEditorController extends Controller
|
|||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json(['error' => "{$action} mislukt"], 500);
|
return response()->json(['error' => "{$action} failed"], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatItemData(\stdClass $item): array
|
private function formatItemData(ItemBase $item): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $item->id,
|
'id' => $item->id,
|
||||||
@@ -59,17 +63,17 @@ class FurniEditorController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatCatalogItemData(\stdClass $r): array
|
private function formatCatalogItemData(CatalogItem $item): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $r->id,
|
'id' => $item->id,
|
||||||
'catalogName' => $r->catalog_name,
|
'catalogName' => $item->catalog_name,
|
||||||
'costCredits' => $r->cost_credits,
|
'costCredits' => $item->cost_credits,
|
||||||
'costPoints' => $r->cost_points,
|
'costPoints' => $item->cost_points,
|
||||||
'pointsType' => $r->points_type,
|
'pointsType' => $item->points_type,
|
||||||
'pageId' => $r->page_id,
|
'pageId' => $item->page_id,
|
||||||
'pageName' => $r->page_id
|
'pageName' => $item->page_id
|
||||||
? DB::table('catalog_pages')->where('id', $r->page_id)->value('caption') ?? ''
|
? CatalogPage::where('id', $item->page_id)->value('caption') ?? ''
|
||||||
: '',
|
: '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -84,7 +88,7 @@ class FurniEditorController extends Controller
|
|||||||
$page = max(1, (int) $request->input('page', 1));
|
$page = max(1, (int) $request->input('page', 1));
|
||||||
$limit = min(50, max(1, (int) $request->input('limit', 20)));
|
$limit = min(50, max(1, (int) $request->input('limit', 20)));
|
||||||
|
|
||||||
$query = DB::table('items_base');
|
$query = ItemBase::query();
|
||||||
|
|
||||||
if ($q !== '' && $q !== '0') {
|
if ($q !== '' && $q !== '0') {
|
||||||
$query->where(function ($w) use ($q) {
|
$query->where(function ($w) use ($q) {
|
||||||
@@ -105,7 +109,7 @@ class FurniEditorController extends Controller
|
|||||||
->skip(($page - 1) * $limit)
|
->skip(($page - 1) * $limit)
|
||||||
->take($limit)
|
->take($limit)
|
||||||
->get()
|
->get()
|
||||||
->map(fn ($r) => [
|
->map(fn (ItemBase $r) => [
|
||||||
'id' => $r->id,
|
'id' => $r->id,
|
||||||
'spriteId' => $r->sprite_id,
|
'spriteId' => $r->sprite_id,
|
||||||
'itemName' => $r->item_name,
|
'itemName' => $r->item_name,
|
||||||
@@ -128,7 +132,7 @@ class FurniEditorController extends Controller
|
|||||||
'page' => $page,
|
'page' => $page,
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Zoeken', $e);
|
return $this->handleApiError('Search', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,15 +146,14 @@ class FurniEditorController extends Controller
|
|||||||
return response()->json(['error' => 'Missing id'], 400);
|
return response()->json(['error' => 'Missing id'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = DB::table('items_base')->where('id', $id)->first();
|
$item = ItemBase::where('id', $id)->first();
|
||||||
if (! $item) {
|
if (! $item) {
|
||||||
return response()->json(['error' => 'Item not found'], 404);
|
return response()->json(['error' => 'Item not found'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$catalog = DB::table('catalog_items')
|
$catalog = CatalogItem::whereRaw('FIND_IN_SET(?, item_ids)', [$id])
|
||||||
->whereRaw('FIND_IN_SET(?, item_ids)', [$id])
|
|
||||||
->get()
|
->get()
|
||||||
->map(fn ($r) => $this->formatCatalogItemData($r));
|
->map(fn (CatalogItem $r) => $this->formatCatalogItemData($r));
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'item' => array_merge($this->formatItemData($item), [
|
'item' => array_merge($this->formatItemData($item), [
|
||||||
@@ -161,7 +164,7 @@ class FurniEditorController extends Controller
|
|||||||
'furniDataEntry' => null,
|
'furniDataEntry' => null,
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Item laden', $e);
|
return $this->handleApiError('Load item', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +185,7 @@ class FurniEditorController extends Controller
|
|||||||
return response()->json(['error' => 'No fields to update'], 400);
|
return response()->json(['error' => 'No fields to update'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::table('items_base')->where('id', $id)->update($update);
|
ItemBase::where('id', $id)->update($update);
|
||||||
|
|
||||||
Log::info('[Audit] FurniEditor update', [
|
Log::info('[Audit] FurniEditor update', [
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
@@ -192,7 +195,7 @@ class FurniEditorController extends Controller
|
|||||||
|
|
||||||
return response()->json(['success' => true]);
|
return response()->json(['success' => true]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Actie', $e);
|
return $this->handleApiError('Update', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,7 @@ class FurniEditorController extends Controller
|
|||||||
try {
|
try {
|
||||||
$data = $request->json()->all();
|
$data = $request->json()->all();
|
||||||
|
|
||||||
$id = DB::table('items_base')->insertGetId($this->buildInsertData($data));
|
$id = ItemBase::insertGetId($this->buildInsertData($data));
|
||||||
|
|
||||||
Log::info('[Audit] FurniEditor create', [
|
Log::info('[Audit] FurniEditor create', [
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
@@ -212,7 +215,7 @@ class FurniEditorController extends Controller
|
|||||||
|
|
||||||
return response()->json(['id' => $id]);
|
return response()->json(['id' => $id]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Actie', $e);
|
return $this->handleApiError('Create', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +229,7 @@ class FurniEditorController extends Controller
|
|||||||
return response()->json(['error' => 'Missing id'], 400);
|
return response()->json(['error' => 'Missing id'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::table('items_base')->where('id', $id)->delete();
|
ItemBase::where('id', $id)->delete();
|
||||||
|
|
||||||
Log::warning('[Audit] FurniEditor delete', [
|
Log::warning('[Audit] FurniEditor delete', [
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
@@ -235,7 +238,7 @@ class FurniEditorController extends Controller
|
|||||||
|
|
||||||
return response()->json(['success' => true]);
|
return response()->json(['success' => true]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Actie', $e);
|
return $this->handleApiError('Delete', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +247,7 @@ class FurniEditorController extends Controller
|
|||||||
$this->checkAdmin();
|
$this->checkAdmin();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$rows = DB::table('items_base')
|
$rows = ItemBase::query()
|
||||||
->select('interaction_type')
|
->select('interaction_type')
|
||||||
->groupBy('interaction_type')
|
->groupBy('interaction_type')
|
||||||
->orderBy('interaction_type')
|
->orderBy('interaction_type')
|
||||||
@@ -252,7 +255,7 @@ class FurniEditorController extends Controller
|
|||||||
|
|
||||||
return response()->json(['interactions' => $rows]);
|
return response()->json(['interactions' => $rows]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Actie', $e);
|
return $this->handleApiError('Interactions', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,14 +269,14 @@ class FurniEditorController extends Controller
|
|||||||
return response()->json(['error' => 'Missing spriteId'], 400);
|
return response()->json(['error' => 'Missing spriteId'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
$item = DB::table('items_base')->where('sprite_id', $spriteId)->first();
|
$item = ItemBase::where('sprite_id', $spriteId)->first();
|
||||||
if (! $item) {
|
if (! $item) {
|
||||||
return response()->json(['error' => 'Item not found'], 404);
|
return response()->json(['error' => 'Item not found'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(['id' => $item->id]);
|
return response()->json(['id' => $item->id]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->handleApiError('Actie', $e);
|
return $this->handleApiError('By sprite', $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Api\HelpTicketReplyRequest;
|
||||||
|
use App\Http\Requests\Api\HelpTicketRequest;
|
||||||
|
use App\Http\Resources\Api\HelpTicketResource;
|
||||||
|
use App\Models\Help\WebsiteHelpCenterTicket;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class HelpApiController extends Controller
|
||||||
|
{
|
||||||
|
public function tickets(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$tickets = WebsiteHelpCenterTicket::with('user:id,username,look')
|
||||||
|
->when($request->user(), fn ($q) => $q->where('user_id', $request->user()->id))
|
||||||
|
->latest()
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => HelpTicketResource::collection($tickets),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $tickets->currentPage(),
|
||||||
|
'last_page' => $tickets->lastPage(),
|
||||||
|
'total' => $tickets->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$ticket = WebsiteHelpCenterTicket::with(['user:id,username,look', 'replies.user:id,username,look'])
|
||||||
|
->where('id', $id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return response()->json(['data' => new HelpTicketResource($ticket)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(HelpTicketRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$ticket = WebsiteHelpCenterTicket::create([
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
'subject' => $validated['subject'],
|
||||||
|
'category' => $validated['category'],
|
||||||
|
'status' => 'open',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket->replies()->create([
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
'message' => $validated['message'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['data' => new HelpTicketResource($ticket)], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reply(HelpTicketReplyRequest $request, string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$ticket = WebsiteHelpCenterTicket::where('id', $id)
|
||||||
|
->where('user_id', $request->user()->id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$reply = $ticket->replies()->create([
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
'message' => $validated['message'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['data' => $reply->load('user:id,username,look')], 201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\Api\HelpTicketReplyRequest;
|
|
||||||
use App\Http\Requests\Api\HelpTicketRequest;
|
|
||||||
use App\Http\Requests\Api\PhotoUploadRequest;
|
|
||||||
use App\Http\Resources\Api\ArticleResource;
|
|
||||||
use App\Http\Resources\Api\HelpTicketResource;
|
|
||||||
use App\Http\Resources\Api\LeaderboardUserResource;
|
|
||||||
use App\Http\Resources\Api\PhotoResource;
|
|
||||||
use App\Http\Resources\Api\ShopPackageResource;
|
|
||||||
use App\Models\Articles\WebsiteArticle;
|
|
||||||
use App\Models\Game\Furniture\CatalogItem;
|
|
||||||
use App\Models\Game\Furniture\CatalogPage;
|
|
||||||
use App\Models\Game\Guild\Guild;
|
|
||||||
use App\Models\Help\WebsiteHelpCenterTicket;
|
|
||||||
use App\Models\Miscellaneous\CameraWeb;
|
|
||||||
use App\Models\Miscellaneous\WebsiteSetting;
|
|
||||||
use App\Models\Shop\WebsiteShopArticle;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Services\Community\StaffService;
|
|
||||||
use App\Services\User\UserApiService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
class HotelApiController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly UserApiService $userApiService,
|
|
||||||
private readonly StaffService $staffService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function fetchUser(string $username): JsonResponse
|
|
||||||
{
|
|
||||||
$user = $this->userApiService->fetchUser($username, ['username', 'motto', 'look']);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $user,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onlineUsers(): JsonResponse
|
|
||||||
{
|
|
||||||
$users = $this->userApiService->onlineUsers(['username', 'motto', 'look'], true);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $users,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onlineUserCount(): JsonResponse
|
|
||||||
{
|
|
||||||
$count = $this->userApiService->onlineUserCount();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => [
|
|
||||||
'onlineCount' => $count,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function articles(): JsonResponse
|
|
||||||
{
|
|
||||||
$articles = WebsiteArticle::with(['user:id,username,look'])
|
|
||||||
->latest('id')
|
|
||||||
->paginate(12);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => ArticleResource::collection($articles),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $articles->currentPage(),
|
|
||||||
'last_page' => $articles->lastPage(),
|
|
||||||
'per_page' => $articles->perPage(),
|
|
||||||
'total' => $articles->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function article(string $slug): JsonResponse
|
|
||||||
{
|
|
||||||
$article = WebsiteArticle::with(['user:id,username,look', 'comments.user:id,username,look'])
|
|
||||||
->where('slug', $slug)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => new ArticleResource($article),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function photos(): JsonResponse
|
|
||||||
{
|
|
||||||
$photos = CameraWeb::query()
|
|
||||||
->where('visible', true)
|
|
||||||
->latest('id')
|
|
||||||
->paginate(12);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => PhotoResource::collection($photos),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $photos->currentPage(),
|
|
||||||
'last_page' => $photos->lastPage(),
|
|
||||||
'per_page' => $photos->perPage(),
|
|
||||||
'total' => $photos->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function staff(): JsonResponse
|
|
||||||
{
|
|
||||||
$employees = $this->staffService->fetchStaffPositions();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $employees,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function shopPackages(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$packages = WebsiteShopArticle::latest('id')->paginate(12);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => ShopPackageResource::collection($packages),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $packages->currentPage(),
|
|
||||||
'last_page' => $packages->lastPage(),
|
|
||||||
'per_page' => $packages->perPage(),
|
|
||||||
'total' => $packages->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function shopCategories(): JsonResponse
|
|
||||||
{
|
|
||||||
return response()->json([
|
|
||||||
'data' => ['furniture', 'badges', 'ranks'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function teams(): JsonResponse
|
|
||||||
{
|
|
||||||
$teams = Guild::with('members')
|
|
||||||
->latest('id')
|
|
||||||
->paginate(12);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $teams->items(),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $teams->currentPage(),
|
|
||||||
'last_page' => $teams->lastPage(),
|
|
||||||
'per_page' => $teams->perPage(),
|
|
||||||
'total' => $teams->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function leaderboard(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$type = $request->query('type', 'credits');
|
|
||||||
$limit = (int) $request->query('limit', 100);
|
|
||||||
|
|
||||||
$validTypes = ['credits', 'pixels'];
|
|
||||||
if (! in_array($type, $validTypes)) {
|
|
||||||
$type = 'credits';
|
|
||||||
}
|
|
||||||
|
|
||||||
$users = User::orderByDesc($type)
|
|
||||||
->limit($limit)
|
|
||||||
->get(['id', 'username', 'look', 'motto', 'credits', 'pixels']);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => LeaderboardUserResource::collection($users),
|
|
||||||
'type' => $type,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rareValues(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$categoryId = $request->query('category');
|
|
||||||
|
|
||||||
$query = CatalogItem::query();
|
|
||||||
|
|
||||||
if ($categoryId) {
|
|
||||||
$query->where('page_id', $categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = $query->with('itemBase')
|
|
||||||
->latest('id')
|
|
||||||
->paginate(24);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $items->items(),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $items->currentPage(),
|
|
||||||
'last_page' => $items->lastPage(),
|
|
||||||
'per_page' => $items->perPage(),
|
|
||||||
'total' => $items->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rareValuesCategories(): JsonResponse
|
|
||||||
{
|
|
||||||
$categories = CatalogPage::where('catalog_name', '!=', '')
|
|
||||||
->where('visible', 1)
|
|
||||||
->orderBy('order_number')
|
|
||||||
->get(['id', 'catalog_name', 'icon']);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $categories,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function settings(): JsonResponse
|
|
||||||
{
|
|
||||||
$settings = Cache::remember('api_all_settings', 60, fn () => WebsiteSetting::all()->pluck('value', 'key'));
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => $settings,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function userProfile(string $username): JsonResponse
|
|
||||||
{
|
|
||||||
$user = User::where('username', $username)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'id' => $user->id,
|
|
||||||
'username' => $user->username,
|
|
||||||
'look' => $user->look,
|
|
||||||
'motto' => $user->motto,
|
|
||||||
'account_created' => $user->account_created,
|
|
||||||
'online' => false,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function helpTickets(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$tickets = WebsiteHelpCenterTicket::with('user:id,username,look')
|
|
||||||
->when($request->user(), fn ($q) => $q->where('user_id', $request->user()->id))
|
|
||||||
->latest()
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'data' => HelpTicketResource::collection($tickets),
|
|
||||||
'meta' => [
|
|
||||||
'current_page' => $tickets->currentPage(),
|
|
||||||
'last_page' => $tickets->lastPage(),
|
|
||||||
'total' => $tickets->total(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function helpTicket(string $id): JsonResponse
|
|
||||||
{
|
|
||||||
$ticket = WebsiteHelpCenterTicket::with(['user:id,username,look', 'replies.user:id,username,look'])
|
|
||||||
->where('id', $id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
return response()->json(['data' => new HelpTicketResource($ticket)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function helpTicketCreate(HelpTicketRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
|
|
||||||
$ticket = WebsiteHelpCenterTicket::create([
|
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'subject' => $validated['subject'],
|
|
||||||
'category' => $validated['category'],
|
|
||||||
'status' => 'open',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$ticket->replies()->create([
|
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'message' => $validated['message'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json(['data' => new HelpTicketResource($ticket)], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function helpTicketReply(HelpTicketReplyRequest $request, string $id): JsonResponse
|
|
||||||
{
|
|
||||||
$ticket = WebsiteHelpCenterTicket::where('id', $id)
|
|
||||||
->where('user_id', $request->user()->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
$reply = $ticket->replies()->create([
|
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'message' => $request->input('message'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json(['data' => $reply->load('user:id,username,look')], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uploadPhoto(PhotoUploadRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$path = $request->file('image')->store('photos', 'public');
|
|
||||||
|
|
||||||
$photo = CameraWeb::create([
|
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'image' => '/storage/' . $path,
|
|
||||||
'visible' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response()->json(['data' => new PhotoResource($photo)], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function purchasePackage(Request $request, int $packageId): JsonResponse
|
|
||||||
{
|
|
||||||
$package = WebsiteShopArticle::findOrFail($packageId);
|
|
||||||
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
$cost = $package->costs;
|
|
||||||
|
|
||||||
if ($user->credits < $cost) {
|
|
||||||
return response()->json(['error' => 'Not enough credits'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->decrement('credits', $cost);
|
|
||||||
|
|
||||||
return response()->json(['success' => true, 'message' => 'Purchase successful']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Api\PhotoUploadRequest;
|
||||||
|
use App\Http\Resources\Api\PhotoResource;
|
||||||
|
use App\Models\Miscellaneous\CameraWeb;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class MediaApiController extends Controller
|
||||||
|
{
|
||||||
|
public function photos(): JsonResponse
|
||||||
|
{
|
||||||
|
$photos = CameraWeb::query()
|
||||||
|
->where('visible', true)
|
||||||
|
->latest('id')
|
||||||
|
->paginate(12);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => PhotoResource::collection($photos),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $photos->currentPage(),
|
||||||
|
'last_page' => $photos->lastPage(),
|
||||||
|
'per_page' => $photos->perPage(),
|
||||||
|
'total' => $photos->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(PhotoUploadRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$path = $request->file('image')->store('photos', 'public');
|
||||||
|
|
||||||
|
$photo = CameraWeb::create([
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
'image' => '/storage/' . $path,
|
||||||
|
'visible' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['data' => new PhotoResource($photo)], 201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Api\ShopPackageResource;
|
||||||
|
use App\Models\Shop\WebsiteShopArticle;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ShopApiController extends Controller
|
||||||
|
{
|
||||||
|
public function packages(): JsonResponse
|
||||||
|
{
|
||||||
|
$packages = WebsiteShopArticle::latest('id')->paginate(12);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => ShopPackageResource::collection($packages),
|
||||||
|
'meta' => [
|
||||||
|
'current_page' => $packages->currentPage(),
|
||||||
|
'last_page' => $packages->lastPage(),
|
||||||
|
'per_page' => $packages->perPage(),
|
||||||
|
'total' => $packages->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function categories(): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'data' => ['furniture', 'badges', 'ranks'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchase(Request $request, int $packageId): JsonResponse
|
||||||
|
{
|
||||||
|
$package = WebsiteShopArticle::findOrFail($packageId);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($package->give_rank && $user->rank >= $package->give_rank) {
|
||||||
|
return response()->json(['error' => 'You already have this or a higher rank'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->credits < $package->costs) {
|
||||||
|
return response()->json(['error' => 'Not enough credits'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->decrement('credits', $package->costs);
|
||||||
|
|
||||||
|
return response()->json(['success' => true, 'message' => 'Purchase successful']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Api\LeaderboardUserResource;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\User\UserApiService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserApiController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserApiService $userApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function fetchUser(string $username): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $this->userApiService->fetchUser($username, ['username', 'motto', 'look']);
|
||||||
|
|
||||||
|
return response()->json(['data' => $user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userProfile(string $username): JsonResponse
|
||||||
|
{
|
||||||
|
$user = User::where('username', $username)->first();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return response()->json(['data' => null], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'username' => $user->username,
|
||||||
|
'look' => $user->look,
|
||||||
|
'motto' => $user->motto,
|
||||||
|
'account_created' => $user->account_created,
|
||||||
|
'online' => false,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onlineUsers(): JsonResponse
|
||||||
|
{
|
||||||
|
$users = $this->userApiService->onlineUsers(['username', 'motto', 'look'], true);
|
||||||
|
|
||||||
|
return response()->json(['data' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onlineUserCount(): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'data' => ['onlineCount' => $this->userApiService->onlineUserCount()],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function leaderboard(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$type = $request->query('type', 'credits');
|
||||||
|
$limit = (int) $request->query('limit', 100);
|
||||||
|
|
||||||
|
if (! in_array($type, ['credits', 'pixels'])) {
|
||||||
|
$type = 'credits';
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = User::orderByDesc($type)
|
||||||
|
->limit($limit)
|
||||||
|
->get(['id', 'username', 'look', 'motto', 'credits', 'pixels']);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => LeaderboardUserResource::collection($users),
|
||||||
|
'type' => $type,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ class BadgeController extends Controller
|
|||||||
{
|
{
|
||||||
public function show(): View
|
public function show(): View
|
||||||
{
|
{
|
||||||
$cost = 150;
|
$cost = (int) setting('badge_cost', 150);
|
||||||
$currencyType = 'credits';
|
$currencyType = 'credits';
|
||||||
$folderError = false;
|
$folderError = false;
|
||||||
$errorMessage = '';
|
$errorMessage = '';
|
||||||
@@ -60,7 +60,7 @@ class BadgeController extends Controller
|
|||||||
return redirect()->route('login')->with('error', 'You must be logged in to purchase badges.');
|
return redirect()->route('login')->with('error', 'You must be logged in to purchase badges.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$cost = 150;
|
$cost = (int) setting('badge_cost', 150);
|
||||||
|
|
||||||
if (property_exists($user, 'credits') && $user->credits !== null && $user->credits < $cost) {
|
if (property_exists($user, 'credits') && $user->credits !== null && $user->credits < $cost) {
|
||||||
return redirect()->back()->with('error', 'You don\'t have enough credits to purchase a badge.');
|
return redirect()->back()->with('error', 'You don\'t have enough credits to purchase a badge.');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Community;
|
|||||||
|
|
||||||
use App\Enums\RadioSettings;
|
use App\Enums\RadioSettings;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||||
use App\Models\Miscellaneous\WebsiteSetting;
|
use App\Models\Miscellaneous\WebsiteSetting;
|
||||||
use App\Models\RadioApplication;
|
use App\Models\RadioApplication;
|
||||||
use App\Models\RadioBanner;
|
use App\Models\RadioBanner;
|
||||||
@@ -20,6 +21,7 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class RadioController extends Controller
|
class RadioController extends Controller
|
||||||
{
|
{
|
||||||
|
use HasRadioSettings;
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RadioStreamService $streamService,
|
private readonly RadioStreamService $streamService,
|
||||||
private readonly RadioScheduleService $scheduleService,
|
private readonly RadioScheduleService $scheduleService,
|
||||||
@@ -120,7 +122,8 @@ class RadioController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validated['rank_id']) {
|
if ($validated['rank_id']) {
|
||||||
$rank = Cache::remember("radio_rank_{$validated['rank_id']}", 300, fn () => RadioRank::find($validated['rank_id']));
|
$acceptingRanks = Cache::remember('radio_ranks_accepting', 300, fn () => RadioRank::where('accepts_applications', true)->get()->keyBy('id'));
|
||||||
|
$rank = $acceptingRanks->get((int) $validated['rank_id']);
|
||||||
|
|
||||||
if (! $rank || ! $rank->accepts_applications) {
|
if (! $rank || ! $rank->accepts_applications) {
|
||||||
return back()->withErrors([
|
return back()->withErrors([
|
||||||
@@ -336,13 +339,4 @@ class RadioController extends Controller
|
|||||||
return WebsiteSetting::whereIn('key', $stringKeys)->pluck('value', 'key')->all();
|
return WebsiteSetting::whereIn('key', $stringKeys)->pluck('value', 'key')->all();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSetting(RadioSettings $setting, mixed $default = null): mixed
|
|
||||||
{
|
|
||||||
return Cache::remember("setting_{$setting->value}", 60, function () use ($setting, $default): mixed {
|
|
||||||
$websiteSetting = WebsiteSetting::where('key', $setting->value)->first();
|
|
||||||
|
|
||||||
return $websiteSetting?->value ?? $default;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Concerns;
|
||||||
|
|
||||||
|
use App\Enums\RadioSettings;
|
||||||
|
use App\Models\Miscellaneous\WebsiteSetting;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
trait HasRadioSettings
|
||||||
|
{
|
||||||
|
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
||||||
|
|
||||||
|
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
||||||
|
$setting = WebsiteSetting::where('key', $keyStr)->first();
|
||||||
|
|
||||||
|
return $setting?->value ?? $default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use App\Models\Miscellaneous\WebsiteSetting;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class LogoGeneratorController extends Controller
|
class LogoGeneratorController extends Controller
|
||||||
@@ -24,9 +25,25 @@ class LogoGeneratorController extends Controller
|
|||||||
|
|
||||||
public function store(Request $request): JsonResponse
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$request->validate(['logo' => 'required|image|mimes:jpeg,png,gif,webp|max:5120']);
|
$request->validate([
|
||||||
|
'logo' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpeg,png,gif,webp',
|
||||||
|
'max:5120',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
$path = $request->file('logo')->store('generated-logos', 'public');
|
$file = $request->file('logo');
|
||||||
|
$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file->getPathname());
|
||||||
|
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
|
||||||
|
if (! in_array($mime, $allowedMimes, true)) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Invalid file type.'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = 'logo_' . Str::random(16) . '.' . $file->getClientOriginalExtension();
|
||||||
|
$path = $file->storeAs('generated-logos', $filename, 'public');
|
||||||
|
|
||||||
$setting = WebsiteSetting::where('key', 'cms_logo')->first();
|
$setting = WebsiteSetting::where('key', 'cms_logo')->first();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
|
|||||||
|
|
||||||
use App\Enums\RadioSettings;
|
use App\Enums\RadioSettings;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Miscellaneous\WebsiteSetting;
|
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||||
use App\Services\Community\RadioStreamService;
|
use App\Services\Community\RadioStreamService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -15,6 +15,7 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class EmbedController extends Controller
|
class EmbedController extends Controller
|
||||||
{
|
{
|
||||||
|
use HasRadioSettings;
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RadioStreamService $streamService,
|
private readonly RadioStreamService $streamService,
|
||||||
) {}
|
) {}
|
||||||
@@ -53,14 +54,4 @@ class EmbedController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
|
||||||
{
|
|
||||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
|
||||||
|
|
||||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
|
||||||
$websiteSetting = WebsiteSetting::where('key', $keyStr)->first();
|
|
||||||
|
|
||||||
return $websiteSetting?->value ?? $default;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
|
|||||||
|
|
||||||
use App\Enums\RadioSettings;
|
use App\Enums\RadioSettings;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Miscellaneous\WebsiteSetting;
|
use App\Http\Controllers\Concerns\HasRadioSettings;
|
||||||
use App\Services\Community\RadioScheduleService;
|
use App\Services\Community\RadioScheduleService;
|
||||||
use App\Services\Community\RadioStreamService;
|
use App\Services\Community\RadioStreamService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
|||||||
|
|
||||||
class SseController extends Controller
|
class SseController extends Controller
|
||||||
{
|
{
|
||||||
|
use HasRadioSettings;
|
||||||
private const int SSE_KEEPALIVE = 15;
|
private const int SSE_KEEPALIVE = 15;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -179,15 +180,4 @@ class SseController extends Controller
|
|||||||
|
|
||||||
return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId;
|
return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSetting(RadioSettings|string $key, mixed $default = null): mixed
|
|
||||||
{
|
|
||||||
$keyStr = $key instanceof RadioSettings ? $key->value : $key;
|
|
||||||
|
|
||||||
return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed {
|
|
||||||
$setting = WebsiteSetting::where('key', $keyStr)->first();
|
|
||||||
|
|
||||||
return $setting?->value ?? $default;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ class AccountSettingsController extends Controller
|
|||||||
return redirect()->back()->withErrors('User not found');
|
return redirect()->back()->withErrors('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->mail !== $request->input('mail')) {
|
$validated = $request->validated();
|
||||||
$this->userService->updateField($user, 'mail', $request->input('mail'));
|
|
||||||
|
if ($user->mail !== $validated['mail']) {
|
||||||
|
$this->userService->updateField($user, 'mail', $validated['mail']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->motto !== $request->input('motto')) {
|
if ($user->motto !== $validated['motto']) {
|
||||||
$this->rconService->setMotto($user, $request->input('motto'));
|
$this->rconService->setMotto($user, $validated['motto']);
|
||||||
$this->userService->updateField($user, 'motto', $request->input('motto'));
|
$this->userService->updateField($user, 'motto', $validated['motto']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated'));
|
return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated'));
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ class GuestbookController extends Controller
|
|||||||
{
|
{
|
||||||
$this->validateGuestbookPost($user, $request);
|
$this->validateGuestbookPost($user, $request);
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
$user->profileGuestbook()->create([
|
$user->profileGuestbook()->create([
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
'message' => $request->input('message'),
|
'message' => $validated['message'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->back()->with('success', __('Your message has been posted.'));
|
return redirect()->back()->with('success', __('Your message has been posted.'));
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ class ProfileController extends Controller
|
|||||||
'badges',
|
'badges',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$showStats = (bool) (WebsiteSetting::where('key', 'profile_show_stats')->first()?->value ?? '1');
|
$settings = WebsiteSetting::whereIn('key', ['profile_show_stats', 'profile_show_online_status'])
|
||||||
$showOnline = (bool) (WebsiteSetting::where('key', 'profile_show_online_status')->first()?->value ?? '1');
|
->pluck('value', 'key');
|
||||||
|
$showStats = (bool) ($settings['profile_show_stats'] ?? '1');
|
||||||
|
$showOnline = (bool) ($settings['profile_show_online_status'] ?? '1');
|
||||||
|
|
||||||
return view('user.profile', [
|
return view('user.profile', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ class RadioApiKey
|
|||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next, string $permission = '*'): Response
|
public function handle(Request $request, Closure $next, string $permission = '*'): Response
|
||||||
{
|
{
|
||||||
$key = $request->bearerToken() ?? $request->query('api_key');
|
$key = $request->bearerToken();
|
||||||
|
|
||||||
if (empty($key)) {
|
if (empty($key)) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'API key is verplicht. Gebruik Authorization: Bearer <key> of ?api_key=<key>',
|
'error' => 'API key is required. Use Authorization: Bearer <key>',
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,19 +25,19 @@ class RadioApiKey
|
|||||||
|
|
||||||
if (! $apiKey) {
|
if (! $apiKey) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'API key is ongeldig of verlopen',
|
'error' => 'API key is invalid or expired',
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $apiKey->isAllowedIp($request->ip())) {
|
if (! $apiKey->isAllowedIp($request->ip())) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'IP-adres niet toegestaan voor deze API key',
|
'error' => 'IP address not allowed for this API key',
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $apiKey->hasPermission($permission)) {
|
if (! $apiKey->hasPermission($permission)) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'error' => 'Geen toestemming voor deze actie',
|
'error' => 'No permission for this action',
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class VPNCheckerMiddleware
|
|||||||
return $this->denyAccess($request);
|
return $this->denyAccess($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ipService = new IpLookupService('');
|
$ipService = new IpLookupService;
|
||||||
|
|
||||||
$countryInfo = $ipService->getCountryInfo($userIp);
|
$countryInfo = $ipService->getCountryInfo($userIp);
|
||||||
|
|
||||||
|
|||||||
+3
-16
@@ -125,10 +125,10 @@ class User extends Authenticatable implements FilamentUser, HasName
|
|||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'team_id', 'avatar_background', 'home_background', 'pincode', 'secret_key', 'extra_rank', 'is_hidden', 'background_id', 'background_stand_id', 'background_overlay_id', 'radio_points', 'pixels', 'points', 'online', 'gender', 'rank', 'mail_verified', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'];
|
protected $fillable = ['username', 'mail', 'password', 'account_created', 'last_login', 'motto', 'look', 'credits', 'last_username_change', 'auth_ticket', 'home_room', 'ip_register', 'ip_current', 'referral_code', 'preferences', 'avatar_background', 'home_background', 'background_id', 'background_stand_id', 'background_overlay_id', 'gender'];
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
protected $hidden = ['id', 'password', 'remember_token'];
|
protected $hidden = ['password', 'remember_token'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
@@ -361,7 +361,7 @@ class User extends Authenticatable implements FilamentUser, HasName
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->update(['two_factor_confirmed' => true]);
|
$this->forceFill(['two_factor_confirmed_at' => now()])->save();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -394,19 +394,6 @@ class User extends Authenticatable implements FilamentUser, HasName
|
|||||||
->logOnlyDirty();
|
->logOnlyDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $options
|
|
||||||
*/
|
|
||||||
#[\Override]
|
|
||||||
public function save(array $options = []): bool
|
|
||||||
{
|
|
||||||
if (! $this->isDirty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::save($options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasAppliedForTeam(int $teamId): bool
|
public function hasAppliedForTeam(int $teamId): bool
|
||||||
{
|
{
|
||||||
if ($teamId === 0) {
|
if ($teamId === 0) {
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Emulator;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class EmulatorBuildService
|
|
||||||
{
|
|
||||||
use EmulatorConfiguration;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->loadConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildFromSource(bool $force = false): array
|
|
||||||
{
|
|
||||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
|
||||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
|
|
||||||
if (! $repo) {
|
|
||||||
return ['success' => false, 'error' => 'Geen source repo geconfigureerd'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourcePath = $this->emulatorSourcePath;
|
|
||||||
$serviceName = $this->emulatorService;
|
|
||||||
|
|
||||||
Log::info('[EmulatorBuild] Starting source build', [
|
|
||||||
'repo' => $repo,
|
|
||||||
'branch' => $branch,
|
|
||||||
'path' => $sourcePath,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->ensureJavaInstalled();
|
|
||||||
|
|
||||||
Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)));
|
|
||||||
Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath));
|
|
||||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)));
|
|
||||||
|
|
||||||
$maxRetries = 2;
|
|
||||||
$lastError = '';
|
|
||||||
|
|
||||||
for ($retry = 0; $retry <= $maxRetries; $retry++) {
|
|
||||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true');
|
|
||||||
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true');
|
|
||||||
|
|
||||||
if ($retry > 0) {
|
|
||||||
Log::info('[EmulatorBuild] Retry attempt', ['attempt' => $retry]);
|
|
||||||
Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'");
|
|
||||||
$sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists';
|
|
||||||
|
|
||||||
$gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'");
|
|
||||||
$isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git';
|
|
||||||
|
|
||||||
if ($sourceExists && $isGitRepo) {
|
|
||||||
Log::info('[EmulatorBuild] Pulling latest changes');
|
|
||||||
$commands = [
|
|
||||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin',
|
|
||||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch),
|
|
||||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
Log::info('[EmulatorBuild] Cloning repository');
|
|
||||||
$commands = [
|
|
||||||
'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath),
|
|
||||||
'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)),
|
|
||||||
'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath),
|
|
||||||
'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$command = implode(' && ', $commands);
|
|
||||||
$result = Process::timeout(300)->run($command);
|
|
||||||
|
|
||||||
if ($result->failed()) {
|
|
||||||
$lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300);
|
|
||||||
Log::warning('[EmulatorBuild] Git operation failed', ['error' => $lastError, 'attempt' => $retry]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$buildCommands = $this->getBuildCommands($sourcePath);
|
|
||||||
|
|
||||||
Log::info('[EmulatorBuild] Running build', ['command' => $buildCommands]);
|
|
||||||
$buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $buildCommands);
|
|
||||||
|
|
||||||
$hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal');
|
|
||||||
|
|
||||||
if ($hasSignalError) {
|
|
||||||
Log::warning('[EmulatorBuild] Build process received signal, checking if JAR was built anyway');
|
|
||||||
}
|
|
||||||
|
|
||||||
$jarPath = $this->findBuiltJar($sourcePath);
|
|
||||||
|
|
||||||
if ($jarPath) {
|
|
||||||
Log::info('[EmulatorBuild] JAR found despite build status', ['jar' => $jarPath]);
|
|
||||||
} elseif ($buildResult->failed() && ! $hasSignalError) {
|
|
||||||
$lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500);
|
|
||||||
Log::warning('[EmulatorBuild] Build failed', ['error' => $lastError, 'attempt' => $retry]);
|
|
||||||
|
|
||||||
if ($retry < $maxRetries) {
|
|
||||||
$cleanCommands = [
|
|
||||||
'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true',
|
|
||||||
];
|
|
||||||
Process::timeout(60)->run(implode(' && ', $cleanCommands));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $jarPath) {
|
|
||||||
$lastError = 'Build succeeded but JAR not found. Check build output.';
|
|
||||||
Log::warning('[EmulatorBuild] JAR not found', ['attempt' => $retry]);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->deployJar($jarPath, $serviceName);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$lastError = $e->getMessage();
|
|
||||||
Log::error('[EmulatorBuild] Source build exception', ['error' => $lastError, 'attempt' => $retry]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBuildCommands(string $sourcePath): string
|
|
||||||
{
|
|
||||||
$pomPath = $sourcePath;
|
|
||||||
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
|
|
||||||
|
|
||||||
if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') {
|
|
||||||
$pomPath = $sourcePath . '/Emulator';
|
|
||||||
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
|
|
||||||
}
|
|
||||||
|
|
||||||
$gradlewPath = $sourcePath . '/gradlew';
|
|
||||||
$gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'");
|
|
||||||
|
|
||||||
$gradlePath = $sourcePath . '/build.gradle';
|
|
||||||
$gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'");
|
|
||||||
|
|
||||||
if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') {
|
|
||||||
return "cd {$pomPath} && mvn clean package -DskipTests 2>&1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') {
|
|
||||||
return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') {
|
|
||||||
return "cd {$sourcePath} && gradle clean build -x test 2>&1";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "cd {$sourcePath} && ls -la";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findBuiltJar(string $sourcePath): ?string
|
|
||||||
{
|
|
||||||
$patterns = [
|
|
||||||
$sourcePath . '/target/*.jar',
|
|
||||||
$sourcePath . '/build/libs/*.jar',
|
|
||||||
$sourcePath . '/*/*.jar',
|
|
||||||
$sourcePath . '/*.jar',
|
|
||||||
$sourcePath . '/Emulator/target/*.jar',
|
|
||||||
$sourcePath . '/Emulator/build/libs/*.jar',
|
|
||||||
$sourcePath . '/Emulator/*/*.jar',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($patterns as $pattern) {
|
|
||||||
$result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1');
|
|
||||||
if ($result->successful()) {
|
|
||||||
$jarPath = trim($result->output());
|
|
||||||
if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) {
|
|
||||||
return $jarPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function deployJar(string $jarPath, string $serviceName): array
|
|
||||||
{
|
|
||||||
$jarName = basename($jarPath);
|
|
||||||
$version = $this->extractVersionFromFilename($jarName);
|
|
||||||
if ($version === '' || $version === '0') {
|
|
||||||
$version = date('Y.m.d');
|
|
||||||
}
|
|
||||||
|
|
||||||
$deployCommands = [
|
|
||||||
'mkdir -p ' . escapeshellarg((string) $this->jarPath),
|
|
||||||
'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath),
|
|
||||||
'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi',
|
|
||||||
'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
|
||||||
'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
|
||||||
'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
|
|
||||||
'ls -la ' . escapeshellarg((string) $this->jarPath) . '/',
|
|
||||||
'systemctl restart ' . escapeshellarg((string) $serviceName) . ' 2>&1 || service ' . escapeshellarg((string) $serviceName) . ' restart 2>&1 || true',
|
|
||||||
];
|
|
||||||
|
|
||||||
$deployCommand = implode(' && ', $deployCommands);
|
|
||||||
|
|
||||||
Log::info('[EmulatorBuild] Deploying jar', [
|
|
||||||
'source' => $jarPath,
|
|
||||||
'destination' => "{$this->jarPath}/{$jarName}",
|
|
||||||
]);
|
|
||||||
|
|
||||||
$deployResult = Process::timeout(60)->run($deployCommand);
|
|
||||||
|
|
||||||
if (! $deployResult->successful()) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceService = new EmulatorSourceService;
|
|
||||||
$sourceInfo = $sourceService->checkForUpdates();
|
|
||||||
$installedDate = $sourceInfo['latest_timestamp'] ?? time();
|
|
||||||
|
|
||||||
$this->settings->set('emulator_version', $version);
|
|
||||||
$this->settings->set('emulator_jar_installed_date', (string) $installedDate);
|
|
||||||
$this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown');
|
|
||||||
$this->settings->set('emulator_source_date', (string) $installedDate);
|
|
||||||
|
|
||||||
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
$this->settings->set('emulator_installed_branch', $currentBranch);
|
|
||||||
|
|
||||||
$sqlService = new EmulatorSqlService;
|
|
||||||
$sqlService->runUpdates();
|
|
||||||
|
|
||||||
Log::info('[EmulatorBuild] Source build successful');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'version' => $version,
|
|
||||||
'jar' => $jarName,
|
|
||||||
'built' => true,
|
|
||||||
'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureJavaInstalled(): void
|
|
||||||
{
|
|
||||||
$javaCheck = Process::timeout(5)->run('java -version 2>&1');
|
|
||||||
if (! $javaCheck->successful()) {
|
|
||||||
Log::warning('[EmulatorBuild] Java not found, attempting to install');
|
|
||||||
Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mavenCheck = Process::timeout(5)->run('which mvn');
|
|
||||||
if (! $mavenCheck->successful()) {
|
|
||||||
Log::warning('[EmulatorBuild] Maven not found, attempting to install');
|
|
||||||
Process::timeout(180)->run('apt-get install -y maven 2>&1');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,509 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Emulator;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class EmulatorJarService
|
|
||||||
{
|
|
||||||
use EmulatorConfiguration;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->loadConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isConfigured(): bool
|
|
||||||
{
|
|
||||||
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForUpdates(): array
|
|
||||||
{
|
|
||||||
if (! $this->isConfigured()) {
|
|
||||||
return [
|
|
||||||
'update_available' => false,
|
|
||||||
'error' => 'Configureer een GitHub URL of directe .jar URL',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceService = new EmulatorSourceService;
|
|
||||||
$sourceInfo = $sourceService->checkForUpdates();
|
|
||||||
$hasSourceUpdates = $sourceInfo && $sourceInfo['has_update'];
|
|
||||||
|
|
||||||
if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) {
|
|
||||||
return $this->checkDirectUrlUpdates($sourceInfo, $hasSourceUpdates);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->checkGitHubFolderUpdates($sourceInfo, $hasSourceUpdates);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function performUpdate(array $check): array
|
|
||||||
{
|
|
||||||
$jarUrl = $check['jar_url'];
|
|
||||||
$jarName = $check['jar_name'];
|
|
||||||
$version = $check['latest_version'];
|
|
||||||
$serviceName = $this->emulatorService;
|
|
||||||
|
|
||||||
$tempDir = '/tmp/emulator-update-' . Str::random(8);
|
|
||||||
$tempJar = $tempDir . '/' . $jarName;
|
|
||||||
|
|
||||||
$updateScript = $this->buildUpdateScript($jarUrl, $jarName, $tempDir, $tempJar, $serviceName);
|
|
||||||
|
|
||||||
$scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh';
|
|
||||||
file_put_contents($scriptPath, $updateScript);
|
|
||||||
chmod($scriptPath, 0755);
|
|
||||||
|
|
||||||
Log::info('[EmulatorJar] Starting update', [
|
|
||||||
'version' => $version,
|
|
||||||
'jar' => $jarName,
|
|
||||||
'url' => $jarUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1');
|
|
||||||
@unlink($scriptPath);
|
|
||||||
|
|
||||||
if ($result->exitCode() !== 0) {
|
|
||||||
Log::error('[EmulatorJar] Update failed', [
|
|
||||||
'output' => $result->output(),
|
|
||||||
'error' => $result->errorOutput(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'Update mislukt: ' . substr($result->output(), 0, 300),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storeUpdateInfo($version, $check);
|
|
||||||
|
|
||||||
Log::info('[EmulatorJar] Update successful');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'version' => $version,
|
|
||||||
'jar' => $jarName,
|
|
||||||
'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart",
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[EmulatorJar] Exception', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findLatestJar(): ?array
|
|
||||||
{
|
|
||||||
if (! $this->githubRepo) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$branch = $this->githubBranch ?: 'main';
|
|
||||||
|
|
||||||
$commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar'];
|
|
||||||
|
|
||||||
foreach ($commonNames as $name) {
|
|
||||||
try {
|
|
||||||
$apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}";
|
|
||||||
$response = Http::timeout(10)
|
|
||||||
->withHeaders([
|
|
||||||
'Accept' => 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
|
||||||
])
|
|
||||||
->get($apiUrl);
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = json_decode($response->body(), true);
|
|
||||||
|
|
||||||
if (! isset($data['sha']) || ! isset($data['download_url'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commitDate = null;
|
|
||||||
$commitSha = $data['sha'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$commitsResponse = Http::timeout(10)
|
|
||||||
->withHeaders([
|
|
||||||
'Accept' => 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
|
||||||
])
|
|
||||||
->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
|
|
||||||
'path' => "Latest_Compiled_Version/{$name}",
|
|
||||||
'sha' => $branch,
|
|
||||||
'per_page' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($commitsResponse->successful()) {
|
|
||||||
$commits = $commitsResponse->json();
|
|
||||||
if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) {
|
|
||||||
$commitDate = strtotime($commits[0]['commit']['committer']['date']);
|
|
||||||
$commitSha = $commits[0]['sha'] ?? $data['sha'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception) {
|
|
||||||
Log::debug('[EmulatorJar] Could not fetch commit date for ' . $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
|
||||||
$installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null);
|
|
||||||
|
|
||||||
$isUpdate = false;
|
|
||||||
if ($installedJarCommit !== null) {
|
|
||||||
$isUpdate = $installedJarCommit !== $commitSha;
|
|
||||||
} elseif ($installedDate !== null && $commitDate !== null) {
|
|
||||||
$isUpdate = (int) $installedDate < $commitDate;
|
|
||||||
} elseif ($installedDate === null && $commitDate !== null) {
|
|
||||||
$isUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$version = $this->extractVersionFromFilename($name);
|
|
||||||
if ($version === '' || $version === '0') {
|
|
||||||
$version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => $name,
|
|
||||||
'url' => $data['download_url'],
|
|
||||||
'version' => $version,
|
|
||||||
'commit' => $commitSha,
|
|
||||||
'commit_date' => $commitDate,
|
|
||||||
'is_update' => $isUpdate,
|
|
||||||
'installed_date' => $installedDate,
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning('[EmulatorJar] Error checking JAR file ' . $name, ['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkDirectUrlUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
|
|
||||||
{
|
|
||||||
$jarInfo = $this->validateDirectUrl($this->jarDirectUrl);
|
|
||||||
|
|
||||||
if ($jarInfo) {
|
|
||||||
if (! empty($jarInfo['version'])) {
|
|
||||||
$currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
|
|
||||||
if ($jarInfo['version'] !== $currentVersion) {
|
|
||||||
$this->settings->set('emulator_version', $jarInfo['version']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$hasJarUpdates = $jarInfo['is_update'] ?? false;
|
|
||||||
|
|
||||||
if ($hasSourceUpdates && ! $hasJarUpdates) {
|
|
||||||
return [
|
|
||||||
'update_available' => true,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'],
|
|
||||||
'release_name' => 'Source Update',
|
|
||||||
'jar_url' => null,
|
|
||||||
'jar_name' => null,
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => 'source_build',
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'update_available' => $hasJarUpdates,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $jarInfo['version'],
|
|
||||||
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL',
|
|
||||||
'jar_url' => $this->jarDirectUrl,
|
|
||||||
'jar_name' => $jarInfo['name'],
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => $hasSourceUpdates ? 'source_build' : 'direct_url',
|
|
||||||
'commit' => $jarInfo['commit_sha'] ?? null,
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'has_source_updates' => $hasSourceUpdates,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasSourceUpdates) {
|
|
||||||
return [
|
|
||||||
'update_available' => true,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
|
|
||||||
'release_name' => 'Source Update',
|
|
||||||
'jar_url' => null,
|
|
||||||
'jar_name' => null,
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => 'source_build',
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'update_available' => false,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'error' => 'Directe .jar URL is niet bereikbaar',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkGitHubFolderUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
|
|
||||||
{
|
|
||||||
$jarInfo = $this->findLatestJar();
|
|
||||||
|
|
||||||
if (! $jarInfo) {
|
|
||||||
if ($hasSourceUpdates) {
|
|
||||||
return [
|
|
||||||
'update_available' => true,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
|
|
||||||
'release_name' => 'Source Update',
|
|
||||||
'jar_url' => null,
|
|
||||||
'jar_name' => null,
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => 'source_build',
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'update_available' => false,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => 'Onbekend',
|
|
||||||
'error' => 'Kon geen .jar bestand vinden',
|
|
||||||
'type' => 'not_found',
|
|
||||||
'source_available' => $sourceInfo !== null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$version = $jarInfo['version'];
|
|
||||||
$hasJarUpdates = $jarInfo['is_update'] ?? false;
|
|
||||||
|
|
||||||
if ($hasSourceUpdates && ! $hasJarUpdates) {
|
|
||||||
return [
|
|
||||||
'update_available' => true,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version,
|
|
||||||
'release_name' => 'Source Update',
|
|
||||||
'jar_url' => null,
|
|
||||||
'jar_name' => null,
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => 'source_build',
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'update_available' => $hasJarUpdates,
|
|
||||||
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
|
|
||||||
'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version,
|
|
||||||
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub',
|
|
||||||
'jar_url' => $jarInfo['url'],
|
|
||||||
'jar_name' => $jarInfo['name'],
|
|
||||||
'jar_size' => 'Onbekend',
|
|
||||||
'type' => $hasSourceUpdates ? 'source_build' : 'github_folder',
|
|
||||||
'commit' => $jarInfo['commit'] ?? null,
|
|
||||||
'commit_date' => $jarInfo['commit_date'] ?? null,
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
'has_source_updates' => $hasSourceUpdates,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateDirectUrl(string $url): ?array
|
|
||||||
{
|
|
||||||
if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$jarName = basename(parse_url($url, PHP_URL_PATH));
|
|
||||||
$version = $this->extractVersionFromFilename($jarName);
|
|
||||||
$storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
|
|
||||||
|
|
||||||
$lastModified = null;
|
|
||||||
$commitSha = null;
|
|
||||||
$gitHubInfoAvailable = false;
|
|
||||||
|
|
||||||
if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) {
|
|
||||||
$owner = $matches[1];
|
|
||||||
$repo = $matches[2];
|
|
||||||
$branch = $matches[3];
|
|
||||||
$path = $matches[4];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$apiResponse = Http::timeout(10)
|
|
||||||
->withHeaders([
|
|
||||||
'Accept' => 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
|
||||||
])
|
|
||||||
->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [
|
|
||||||
'ref' => $branch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($apiResponse->successful()) {
|
|
||||||
$data = $apiResponse->json();
|
|
||||||
|
|
||||||
if (isset($data['sha']) && ! isset($data['message'])) {
|
|
||||||
$gitHubInfoAvailable = true;
|
|
||||||
$commitSha = $data['sha'];
|
|
||||||
|
|
||||||
if (isset($data['commit']['committer']['date'])) {
|
|
||||||
$lastModified = strtotime($data['commit']['committer']['date']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception) {
|
|
||||||
Log::debug('[EmulatorJar] Could not fetch GitHub commit info for direct URL');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lastModified === null) {
|
|
||||||
$response = Http::timeout(10)->head($url);
|
|
||||||
if ($response->successful()) {
|
|
||||||
$modifiedSince = $response->header('Last-Modified');
|
|
||||||
if ($modifiedSince) {
|
|
||||||
$lastModified = strtotime($modifiedSince);
|
|
||||||
if ($lastModified === false) {
|
|
||||||
$lastModified = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$isUpdate = false;
|
|
||||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
|
||||||
|
|
||||||
$storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url));
|
|
||||||
if ($storedData !== null && is_string($storedData)) {
|
|
||||||
$storedDataArray = json_decode($storedData, true);
|
|
||||||
if (is_array($storedDataArray)) {
|
|
||||||
if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) {
|
|
||||||
$isUpdate = $commitSha !== $storedDataArray['commit_sha'];
|
|
||||||
} elseif ($lastModified !== null && $installedDate !== null) {
|
|
||||||
$isUpdate = (int) $installedDate < $lastModified;
|
|
||||||
} elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) {
|
|
||||||
$isUpdate = $lastModified > $storedDataArray['last_modified'];
|
|
||||||
} elseif (! in_array($version, ['', '0', $storedVersion], true)) {
|
|
||||||
$isUpdate = version_compare($version, $storedVersion) > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$isUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$infoToStore = [
|
|
||||||
'last_checked' => time(),
|
|
||||||
'version' => $version,
|
|
||||||
];
|
|
||||||
if ($lastModified) {
|
|
||||||
$infoToStore['last_modified'] = $lastModified;
|
|
||||||
}
|
|
||||||
if ($commitSha) {
|
|
||||||
$infoToStore['commit_sha'] = $commitSha;
|
|
||||||
}
|
|
||||||
$this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => $jarName,
|
|
||||||
'version' => $version,
|
|
||||||
'last_modified' => $lastModified,
|
|
||||||
'commit_sha' => $commitSha,
|
|
||||||
'is_update' => $isUpdate,
|
|
||||||
'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url),
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning('[EmulatorJar] Direct URL not reachable', ['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildUpdateScript(string $jarUrl, string $jarName, string $tempDir, string $tempJar, string $serviceName): string
|
|
||||||
{
|
|
||||||
return <<<BASH
|
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
JAR_URL='{$jarUrl}'
|
|
||||||
JAR_NAME='{$jarName}'
|
|
||||||
JAR_PATH='{$this->jarPath}'
|
|
||||||
SERVICE='{$serviceName}'
|
|
||||||
TEMP_DIR='{$tempDir}'
|
|
||||||
TEMP_JAR='{$tempJar}'
|
|
||||||
|
|
||||||
mkdir -p "\$TEMP_DIR" "\$JAR_PATH"
|
|
||||||
|
|
||||||
if ls "\$JAR_PATH"/*.jar 1>/dev/null 2>&1; then
|
|
||||||
mkdir -p "\$JAR_PATH/backup"
|
|
||||||
mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
download_success=false
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then
|
|
||||||
FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null)
|
|
||||||
JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then
|
|
||||||
download_success=true
|
|
||||||
break
|
|
||||||
else
|
|
||||||
rm -f "\$TEMP_JAR" 2>/dev/null || true
|
|
||||||
sleep 3
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "\$download_success" = false ]; then
|
|
||||||
if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then
|
|
||||||
mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
rm -rf "\$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mv "\$TEMP_JAR" "\$JAR_PATH/"
|
|
||||||
chown -R www-data:www-data "\$JAR_PATH"
|
|
||||||
chmod 755 "\$JAR_PATH/\$JAR_NAME"
|
|
||||||
|
|
||||||
systemctl restart "\$SERVICE" 2>&1 || service "\$SERVICE" restart 2>&1 || true
|
|
||||||
|
|
||||||
rm -rf "\$TEMP_DIR" 2>/dev/null || true
|
|
||||||
BASH;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function storeUpdateInfo(string $version, array $check): void
|
|
||||||
{
|
|
||||||
setting('emulator_version', $version);
|
|
||||||
|
|
||||||
$commitDate = $check['commit_date'] ?? time();
|
|
||||||
$this->settings->set('emulator_jar_installed_date', (string) $commitDate);
|
|
||||||
$this->settings->set('emulator_jar_commit', $check['commit'] ?? null);
|
|
||||||
|
|
||||||
$sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null;
|
|
||||||
$sourceDate = $check['source_info']['latest_timestamp'] ?? ($check['commit_date'] ?? time());
|
|
||||||
if ($sourceSha) {
|
|
||||||
$this->settings->set('emulator_source_commit', $sourceSha);
|
|
||||||
}
|
|
||||||
if ($sourceDate) {
|
|
||||||
$this->settings->set('emulator_source_date', (string) $sourceDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
$this->settings->set('emulator_installed_branch', $currentBranch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Emulator;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class EmulatorSourceService
|
|
||||||
{
|
|
||||||
use EmulatorConfiguration;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->loadConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForUpdates(): ?array
|
|
||||||
{
|
|
||||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
|
||||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
|
|
||||||
if (! $repo) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$localCheck = $this->checkLocalSourceUpdates();
|
|
||||||
if ($localCheck !== null) {
|
|
||||||
return $localCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->checkRemoteSourceUpdates($repo, $branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isSourceBuildAvailable(): bool
|
|
||||||
{
|
|
||||||
$hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true);
|
|
||||||
$hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true);
|
|
||||||
|
|
||||||
if (! $hasRepo || ! $hasPath) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'");
|
|
||||||
|
|
||||||
return trim($result->output()) === 'exists';
|
|
||||||
} catch (\Exception) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkLocalSourceUpdates(): ?array
|
|
||||||
{
|
|
||||||
$sourcePath = $this->emulatorSourcePath;
|
|
||||||
|
|
||||||
$existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'");
|
|
||||||
if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') {
|
|
||||||
return $this->checkSourceByCloning();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null");
|
|
||||||
if (! $gitCheck->successful()) {
|
|
||||||
return $this->checkSourceByCloning();
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD");
|
|
||||||
if (! $currentCommitResult->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$currentSha = trim($currentCommitResult->output());
|
|
||||||
|
|
||||||
$fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1");
|
|
||||||
if ($fetchResult->failed()) {
|
|
||||||
Log::debug('[EmulatorSource] Git fetch failed, trying local check only');
|
|
||||||
}
|
|
||||||
|
|
||||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
$latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD");
|
|
||||||
if (! $latestResult->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$latestSha = trim($latestResult->output());
|
|
||||||
|
|
||||||
$dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}");
|
|
||||||
$latestDate = null;
|
|
||||||
$latestTimestamp = time();
|
|
||||||
if ($dateResult->successful()) {
|
|
||||||
$latestDate = trim($dateResult->output(), "'");
|
|
||||||
if ($latestDate !== '' && $latestDate !== '0') {
|
|
||||||
$latestTimestamp = strtotime($latestDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
|
||||||
|
|
||||||
$msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}");
|
|
||||||
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
|
|
||||||
|
|
||||||
$authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}");
|
|
||||||
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
|
|
||||||
|
|
||||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
|
||||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
|
||||||
|
|
||||||
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
|
|
||||||
if ($storedSha === null) {
|
|
||||||
$isUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'has_update' => $isUpdate,
|
|
||||||
'latest_sha' => $latestSha,
|
|
||||||
'latest_date' => $latestDate,
|
|
||||||
'latest_timestamp' => $latestTimestamp,
|
|
||||||
'latest_message' => $latestMessage,
|
|
||||||
'latest_author' => $latestAuthor,
|
|
||||||
'stored_sha' => $storedSha,
|
|
||||||
'stored_date' => $storedDate,
|
|
||||||
'source' => 'local',
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug('[EmulatorSource] Local source check failed: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkSourceByCloning(): ?array
|
|
||||||
{
|
|
||||||
$repo = $this->sourceRepo ?: $this->githubRepo;
|
|
||||||
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
|
|
||||||
|
|
||||||
if (! $repo) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo);
|
|
||||||
$repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo);
|
|
||||||
$repo = rtrim($repo, '/');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$tempDir = '/tmp/emulator-source-check-' . Str::random(8);
|
|
||||||
|
|
||||||
$cloneResult = Process::timeout(120)->run(
|
|
||||||
"git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1",
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($cloneResult->failed()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD");
|
|
||||||
$latestSha = $shaResult->successful() ? trim($shaResult->output()) : '';
|
|
||||||
|
|
||||||
$dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'");
|
|
||||||
$latestDate = null;
|
|
||||||
$latestTimestamp = time();
|
|
||||||
if ($dateResult->successful()) {
|
|
||||||
$latestDate = trim($dateResult->output());
|
|
||||||
if ($latestDate !== '' && $latestDate !== '0') {
|
|
||||||
$latestTimestamp = strtotime($latestDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
|
||||||
|
|
||||||
$msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'");
|
|
||||||
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
|
|
||||||
|
|
||||||
$authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'");
|
|
||||||
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
|
|
||||||
|
|
||||||
Process::timeout(10)->run("rm -rf {$tempDir}");
|
|
||||||
|
|
||||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
|
||||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
|
||||||
|
|
||||||
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
|
|
||||||
if ($storedSha === null) {
|
|
||||||
$isUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'has_update' => $isUpdate,
|
|
||||||
'latest_sha' => $latestSha,
|
|
||||||
'latest_date' => $latestDate,
|
|
||||||
'latest_timestamp' => $latestTimestamp,
|
|
||||||
'latest_message' => $latestMessage,
|
|
||||||
'latest_author' => $latestAuthor,
|
|
||||||
'stored_sha' => $storedSha,
|
|
||||||
'stored_date' => $storedDate,
|
|
||||||
'source' => 'cloned',
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::debug('[EmulatorSource] Source clone check failed: ' . $e->getMessage());
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkRemoteSourceUpdates(string $repo, string $branch): ?array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$response = Http::timeout(10)
|
|
||||||
->withHeaders([
|
|
||||||
'Accept' => 'application/vnd.github.v3+json',
|
|
||||||
'User-Agent' => 'AtomCMS-Emulator-Updater',
|
|
||||||
])
|
|
||||||
->get("https://api.github.com/repos/{$repo}/commits", [
|
|
||||||
'sha' => $branch,
|
|
||||||
'per_page' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commits = $response->json();
|
|
||||||
|
|
||||||
if (empty($commits) || ! isset($commits[0]['sha'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$latestCommit = $commits[0];
|
|
||||||
$latestSha = $latestCommit['sha'];
|
|
||||||
$latestDate = $latestCommit['commit']['committer']['date'] ?? null;
|
|
||||||
$latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time();
|
|
||||||
|
|
||||||
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
|
|
||||||
|
|
||||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
|
||||||
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
|
|
||||||
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
|
|
||||||
|
|
||||||
if ($storedSha !== null && $storedSha === $latestSha) {
|
|
||||||
$isUpdate = false;
|
|
||||||
} elseif ($storedSha !== null && $storedSha !== $latestSha) {
|
|
||||||
$isUpdate = true;
|
|
||||||
} elseif ($installedDate !== null) {
|
|
||||||
$installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate);
|
|
||||||
$isUpdate = $installedTimestamp < $latestTimestamp;
|
|
||||||
} else {
|
|
||||||
$isUpdate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'has_update' => $isUpdate,
|
|
||||||
'latest_sha' => $latestSha,
|
|
||||||
'latest_date' => $latestDate,
|
|
||||||
'latest_timestamp' => $latestTimestamp,
|
|
||||||
'latest_message' => $latestCommit['commit']['message'] ?? '',
|
|
||||||
'latest_author' => $latestCommit['commit']['author']['name'] ?? '',
|
|
||||||
'stored_sha' => $storedSha,
|
|
||||||
'stored_date' => $storedDate,
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning('[EmulatorSource] Could not check source updates via GitHub API', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function persistSourceCommitInfo(string $sha, int $timestamp): void
|
|
||||||
{
|
|
||||||
$this->settings->set('emulator_source_commit', $sha);
|
|
||||||
$this->settings->set('emulator_source_date', (string) $timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\Emulator;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
class EmulatorSqlService
|
|
||||||
{
|
|
||||||
use EmulatorConfiguration;
|
|
||||||
|
|
||||||
private const string SQL_TABLE = 'emulator_sql_updates';
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->loadConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isConfigured(): bool
|
|
||||||
{
|
|
||||||
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForUpdates(bool $recentOnly = true): array
|
|
||||||
{
|
|
||||||
if (! $this->githubRepo) {
|
|
||||||
return [
|
|
||||||
'has_updates' => false,
|
|
||||||
'error' => 'Geen GitHub repo geconfigureerd',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->ensureSqlTableExists();
|
|
||||||
|
|
||||||
$result = $this->fetchSqlFilesFromGitHub($recentOnly);
|
|
||||||
|
|
||||||
if (isset($result['error'])) {
|
|
||||||
return [
|
|
||||||
'has_updates' => false,
|
|
||||||
'error' => $result['error'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlFiles = $result;
|
|
||||||
|
|
||||||
if ($sqlFiles === []) {
|
|
||||||
return [
|
|
||||||
'has_updates' => false,
|
|
||||||
'message' => $recentOnly
|
|
||||||
? 'Geen SQL updates van de afgelopen week gevonden'
|
|
||||||
: 'Geen SQL updates gevonden',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$appliedHashes = $this->getAppliedSqlHashes();
|
|
||||||
$newSqlFiles = [];
|
|
||||||
$alreadyApplied = [];
|
|
||||||
|
|
||||||
foreach ($sqlFiles as $file) {
|
|
||||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
|
||||||
if (in_array($hash, $appliedHashes)) {
|
|
||||||
$alreadyApplied[] = $file['name'];
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$newSqlFiles[] = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($newSqlFiles === []) {
|
|
||||||
return [
|
|
||||||
'has_updates' => false,
|
|
||||||
'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)',
|
|
||||||
'applied_count' => count($alreadyApplied),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name']));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'has_updates' => true,
|
|
||||||
'count' => count($newSqlFiles),
|
|
||||||
'files' => $newSqlFiles,
|
|
||||||
'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week',
|
|
||||||
'applied_count' => count($alreadyApplied),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function runUpdates(): array
|
|
||||||
{
|
|
||||||
$sqlCheck = $this->checkForUpdates(false);
|
|
||||||
|
|
||||||
if (! ($sqlCheck['has_updates'] ?? false)) {
|
|
||||||
return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$results = [
|
|
||||||
'success' => true,
|
|
||||||
'sql_updated' => true,
|
|
||||||
'files_run' => [],
|
|
||||||
'errors' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($sqlCheck['files'] as $file) {
|
|
||||||
$sqlResult = $this->downloadAndRunSql($file);
|
|
||||||
|
|
||||||
if ($sqlResult['success']) {
|
|
||||||
$results['files_run'][] = $file['name'];
|
|
||||||
$this->markSqlAsApplied($file);
|
|
||||||
} else {
|
|
||||||
$results['errors'][] = $file['name'] . ': ' . $sqlResult['error'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($results['errors']) > 0) {
|
|
||||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten';
|
|
||||||
} else {
|
|
||||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAppliedUpdates(): array
|
|
||||||
{
|
|
||||||
$this->ensureSqlTableExists();
|
|
||||||
|
|
||||||
return DB::table(self::SQL_TABLE)
|
|
||||||
->orderBy('applied_at', 'desc')
|
|
||||||
->get()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function diagnose(): array
|
|
||||||
{
|
|
||||||
$diagnosis = [
|
|
||||||
'table_exists' => false,
|
|
||||||
'applied_count' => 0,
|
|
||||||
'pending_count' => 0,
|
|
||||||
'error' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE);
|
|
||||||
|
|
||||||
if ($diagnosis['table_exists']) {
|
|
||||||
$diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->githubRepo && $diagnosis['table_exists']) {
|
|
||||||
$sqlCheck = $this->checkForUpdates(false);
|
|
||||||
if (isset($sqlCheck['count'])) {
|
|
||||||
$diagnosis['pending_count'] = $sqlCheck['count'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$diagnosis['error'] = $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $diagnosis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function repair(): array
|
|
||||||
{
|
|
||||||
$actions = [];
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->ensureSqlTableExists();
|
|
||||||
$actions[] = 'SQL update tabel gecontroleerd';
|
|
||||||
|
|
||||||
$appliedCount = DB::table(self::SQL_TABLE)->count();
|
|
||||||
$actions[] = "SQL updates tabel OK ({$appliedCount} records)";
|
|
||||||
|
|
||||||
if ($this->githubRepo) {
|
|
||||||
$sqlCheck = $this->checkForUpdates(false);
|
|
||||||
|
|
||||||
if (isset($sqlCheck['error'])) {
|
|
||||||
$actions[] = 'SQL update check: ' . $sqlCheck['error'];
|
|
||||||
} elseif (! ($sqlCheck['has_updates'] ?? false)) {
|
|
||||||
$actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date');
|
|
||||||
} else {
|
|
||||||
$count = $sqlCheck['count'] ?? 0;
|
|
||||||
$actions[] = "{$count} nieuwe SQL updates beschikbaar";
|
|
||||||
|
|
||||||
if ($this->isConfigured()) {
|
|
||||||
$actions[] = 'SQL updates worden toegepast...';
|
|
||||||
$runResult = $this->runUpdates();
|
|
||||||
|
|
||||||
if ($runResult['success'] ?? false) {
|
|
||||||
$filesRun = count($runResult['files_run'] ?? []);
|
|
||||||
$actions[] = "{$filesRun} SQL updates toegepast";
|
|
||||||
} else {
|
|
||||||
$errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$errors[] = 'SQL repair: ' . $e->getMessage();
|
|
||||||
Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => $errors === [],
|
|
||||||
'actions' => $actions,
|
|
||||||
'errors' => $errors,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureSqlTableExists(): void
|
|
||||||
{
|
|
||||||
if (Schema::hasTable(self::SQL_TABLE)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Schema::create(self::SQL_TABLE, function ($table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('file_name');
|
|
||||||
$table->string('file_hash')->unique();
|
|
||||||
$table->timestamp('applied_at');
|
|
||||||
$table->text('sql_content')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getAppliedSqlHashes(): array
|
|
||||||
{
|
|
||||||
$this->ensureSqlTableExists();
|
|
||||||
|
|
||||||
return DB::table(self::SQL_TABLE)
|
|
||||||
->pluck('file_hash')
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function markSqlAsApplied(array $file): void
|
|
||||||
{
|
|
||||||
$this->ensureSqlTableExists();
|
|
||||||
|
|
||||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
|
||||||
|
|
||||||
DB::table(self::SQL_TABLE)->updateOrInsert(
|
|
||||||
['file_hash' => $hash],
|
|
||||||
[
|
|
||||||
'file_name' => $file['name'],
|
|
||||||
'applied_at' => now(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array
|
|
||||||
{
|
|
||||||
if (! $this->githubRepo) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$branch = $this->githubBranch ?: 'main';
|
|
||||||
|
|
||||||
$folderNames = [
|
|
||||||
'Database%20Updates',
|
|
||||||
'Database Updates',
|
|
||||||
'database_updates',
|
|
||||||
'database/updates',
|
|
||||||
'sql/updates',
|
|
||||||
'sql',
|
|
||||||
'updates',
|
|
||||||
];
|
|
||||||
|
|
||||||
$knownSqlFiles = [
|
|
||||||
'07012026_UpdateDatabase_to_4-0-1.sql',
|
|
||||||
'09012026_UpdateDatabase_to_4-0-2.sql',
|
|
||||||
'12012026_Battle Banzai.sql',
|
|
||||||
'12012026_Breeding Fixes.sql',
|
|
||||||
'12012026_ChatBubbles.sql',
|
|
||||||
'16032026_updateall_command.sql',
|
|
||||||
'17032026_allow_underpass.sql',
|
|
||||||
'19032026_hotel_timezone.sql',
|
|
||||||
'21022026_user_prefixes.sql',
|
|
||||||
'Default_Camera.sql',
|
|
||||||
'UpdateDatabase_Allow_diagonale.sql',
|
|
||||||
'UpdateDatabase_BOT.sql',
|
|
||||||
'UpdateDatabase_Banners.sql',
|
|
||||||
'UpdateDatabase_DanceCMD.sql',
|
|
||||||
'UpdateDatabase_Happiness.sql',
|
|
||||||
'UpdateDatabase_Websocket.sql',
|
|
||||||
'UpdateDatabase_unignorable.sql',
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($recentOnly) {
|
|
||||||
return $this->fetchRecentSqlFiles($branch);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlFiles = [];
|
|
||||||
|
|
||||||
foreach ($knownSqlFiles as $filename) {
|
|
||||||
foreach ($folderNames as $folderName) {
|
|
||||||
$encodedFilename = str_replace(' ', '%20', $filename);
|
|
||||||
$url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}";
|
|
||||||
$response = Http::timeout(10)->get($url);
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
|
||||||
$sqlFiles[] = [
|
|
||||||
'name' => $filename,
|
|
||||||
'url' => $url,
|
|
||||||
'sha' => md5($response->body()),
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sqlFiles !== []) {
|
|
||||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
|
||||||
|
|
||||||
return $sqlFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fetchRecentSqlFiles(string $branch): array
|
|
||||||
{
|
|
||||||
$weekAgo = now()->subDays(7)->toIso8601String();
|
|
||||||
|
|
||||||
$githubToken = setting('github_token', '');
|
|
||||||
$headers = [
|
|
||||||
'Accept' => 'application/vnd.github+json',
|
|
||||||
'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0',
|
|
||||||
];
|
|
||||||
if (! empty($githubToken)) {
|
|
||||||
$headers['Authorization'] = 'Bearer ' . $githubToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
$folderNames = ['Database Updates', 'database_updates', 'sql', 'updates'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach ($folderNames as $folderName) {
|
|
||||||
$response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
|
|
||||||
'path' => $folderName,
|
|
||||||
'sha' => $branch,
|
|
||||||
'since' => $weekAgo,
|
|
||||||
'per_page' => 100,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($response->successful()) {
|
|
||||||
$commits = $response->json();
|
|
||||||
|
|
||||||
if (! is_array($commits) || $commits === []) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlFiles = [];
|
|
||||||
|
|
||||||
foreach ($commits as $commit) {
|
|
||||||
$sha = $commit['sha'] ?? null;
|
|
||||||
$commitDate = $commit['commit']['committer']['date'] ?? null;
|
|
||||||
|
|
||||||
if (! $sha) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}");
|
|
||||||
|
|
||||||
if (! $commitResponse->successful()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commitData = $commitResponse->json();
|
|
||||||
$files = $commitData['files'] ?? [];
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$filename = $file['filename'] ?? '';
|
|
||||||
|
|
||||||
if (! str_ends_with(strtolower((string) $filename), '.sql')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = basename((string) $filename);
|
|
||||||
$sqlFiles[] = [
|
|
||||||
'name' => $name,
|
|
||||||
'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}",
|
|
||||||
'sha' => $sha,
|
|
||||||
'date' => $commitDate,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sqlFiles !== []) {
|
|
||||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
|
||||||
|
|
||||||
return $sqlFiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function downloadAndRunSql(array $file): array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$response = Http::timeout(60)->get($file['url']);
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
return ['success' => false, 'error' => 'Download mislukt'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = $this->cleanSql($response->body());
|
|
||||||
|
|
||||||
if (in_array(trim($sql), ['', '0'], true)) {
|
|
||||||
return ['success' => true, 'message' => 'Lege SQL file overgeslagen'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1'));
|
|
||||||
$port = setting('emulator_database_port', config('database.connections.emulator.port', '3306'));
|
|
||||||
$name = setting('emulator_database_name', config('database.connections.emulator.database', ''));
|
|
||||||
$username = setting('emulator_database_username', config('database.connections.emulator.username', ''));
|
|
||||||
$password = setting('emulator_database_password', config('database.connections.emulator.password', ''));
|
|
||||||
|
|
||||||
if (empty($name) || empty($username)) {
|
|
||||||
return ['success' => false, 'error' => 'Emulator database niet geconfigureerd'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = new \PDO(
|
|
||||||
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
|
|
||||||
$username,
|
|
||||||
$password,
|
|
||||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
|
|
||||||
);
|
|
||||||
|
|
||||||
$statements = $this->splitSqlStatements($sql);
|
|
||||||
$successCount = 0;
|
|
||||||
$skipCount = 0;
|
|
||||||
|
|
||||||
foreach ($statements as $statement) {
|
|
||||||
$statement = trim((string) $statement);
|
|
||||||
if ($statement === '' || $statement === '0') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo->exec($statement);
|
|
||||||
$successCount++;
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if ($this->isDuplicateError($e)) {
|
|
||||||
$skipCount++;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Log::warning('[SQL Update] Statement error: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)");
|
|
||||||
|
|
||||||
return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return ['success' => false, 'error' => $e->getMessage()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanSql(string $sql): string
|
|
||||||
{
|
|
||||||
$sql = preg_replace('/--.*$/m', '', $sql);
|
|
||||||
$sql = preg_replace('/#.*$/m', '', (string) $sql);
|
|
||||||
$sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql);
|
|
||||||
|
|
||||||
return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function splitSqlStatements(string $sql): array
|
|
||||||
{
|
|
||||||
$parts = explode(';', $sql);
|
|
||||||
|
|
||||||
return array_filter($parts, fn ($part) => trim((string) $part) !== '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isDuplicateError(\PDOException $e): bool
|
|
||||||
{
|
|
||||||
$message = strtolower($e->getMessage());
|
|
||||||
$patterns = [
|
|
||||||
'duplicate entry',
|
|
||||||
"table '",
|
|
||||||
'already exists',
|
|
||||||
"can't create",
|
|
||||||
'duplicate key',
|
|
||||||
'duplicate index',
|
|
||||||
'error 1061',
|
|
||||||
'error 1062',
|
|
||||||
'error 1826',
|
|
||||||
];
|
|
||||||
|
|
||||||
return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use App\Models\Miscellaneous\WebsiteSetting;
|
|
||||||
use App\Services\Emulator\EmulatorBackupService;
|
|
||||||
use App\Services\Emulator\EmulatorBuildService;
|
|
||||||
use App\Services\Emulator\EmulatorJarService;
|
|
||||||
use App\Services\Emulator\EmulatorSourceService;
|
|
||||||
use App\Services\Emulator\EmulatorSqlService;
|
|
||||||
use App\Services\Emulator\EmulatorStatusService;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
|
|
||||||
class EmulatorUpdateService
|
|
||||||
{
|
|
||||||
private readonly EmulatorStatusService $statusService;
|
|
||||||
|
|
||||||
private readonly EmulatorJarService $jarService;
|
|
||||||
|
|
||||||
private readonly EmulatorSourceService $sourceService;
|
|
||||||
|
|
||||||
private readonly EmulatorBuildService $buildService;
|
|
||||||
|
|
||||||
private readonly EmulatorSqlService $sqlService;
|
|
||||||
|
|
||||||
private readonly EmulatorBackupService $backupService;
|
|
||||||
|
|
||||||
private readonly SettingsService $settings;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->statusService = new EmulatorStatusService;
|
|
||||||
$this->jarService = new EmulatorJarService;
|
|
||||||
$this->sourceService = new EmulatorSourceService;
|
|
||||||
$this->buildService = new EmulatorBuildService;
|
|
||||||
$this->sqlService = new EmulatorSqlService;
|
|
||||||
$this->backupService = new EmulatorBackupService;
|
|
||||||
$this->settings = app(SettingsService::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isConfigured(): bool
|
|
||||||
{
|
|
||||||
return $this->statusService->isConfigured();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStatus(): array
|
|
||||||
{
|
|
||||||
$status = $this->statusService->getStatus();
|
|
||||||
$updateCheck = $this->jarService->checkForUpdates();
|
|
||||||
$sourceInfo = $this->sourceService->checkForUpdates();
|
|
||||||
|
|
||||||
return array_merge($status, [
|
|
||||||
'update_available' => $updateCheck['update_available'] ?? false,
|
|
||||||
'current_version' => $updateCheck['current_version'] ?? setting('emulator_version', 'N/A'),
|
|
||||||
'latest_version' => $updateCheck['latest_version'] ?? 'N/A',
|
|
||||||
'update_type' => $updateCheck['type'] ?? 'unknown',
|
|
||||||
'has_source_updates' => $sourceInfo['has_update'] ?? false,
|
|
||||||
'latest_sha' => $sourceInfo['latest_sha'] ?? null,
|
|
||||||
'latest_message' => $sourceInfo['latest_message'] ?? null,
|
|
||||||
'latest_author' => $sourceInfo['latest_author'] ?? null,
|
|
||||||
'latest_date' => $sourceInfo['latest_date'] ?? null,
|
|
||||||
'stored_sha' => $sourceInfo['stored_sha'] ?? null,
|
|
||||||
'stored_date' => $sourceInfo['stored_date'] ?? null,
|
|
||||||
'source_info' => $sourceInfo,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForUpdates(): array
|
|
||||||
{
|
|
||||||
return $this->jarService->checkForUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkForSqlUpdates(bool $recentOnly = true): array
|
|
||||||
{
|
|
||||||
return $this->sqlService->checkForUpdates($recentOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function runSqlUpdates(): array
|
|
||||||
{
|
|
||||||
return $this->sqlService->runUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAppliedSqlUpdates(): array
|
|
||||||
{
|
|
||||||
return $this->sqlService->getAppliedUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateEmulator(): array
|
|
||||||
{
|
|
||||||
if (! $this->isConfigured()) {
|
|
||||||
return ['success' => false, 'error' => 'Geen GitHub URL geconfigureerd'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$check = $this->checkForUpdates();
|
|
||||||
|
|
||||||
if (! ($check['update_available'] ?? false)) {
|
|
||||||
if ($check['type'] === 'not_found' && ($check['source_available'] ?? false)) {
|
|
||||||
return $this->buildFromSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['success' => false, 'error' => 'Emulator is al up-to-date'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$hasSourceUpdates = ($check['has_source_updates'] ?? false) || ($check['type'] ?? '') === 'source_build';
|
|
||||||
|
|
||||||
if ($hasSourceUpdates && $this->sourceService->isSourceBuildAvailable()) {
|
|
||||||
return $this->buildFromSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($check['type'] === 'source_build') {
|
|
||||||
return $this->buildFromSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($check['jar_url'] ?? null)) {
|
|
||||||
return ['success' => false, 'error' => 'Geen .jar gevonden'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->jarService->performUpdate($check);
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$this->runSqlUpdates();
|
|
||||||
|
|
||||||
if ($this->restartEmulator()) {
|
|
||||||
$result['restarted'] = true;
|
|
||||||
$result['message'] = ($result['message'] ?? '') . ' | 🔄 Emulator herstart';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function performUpdate(array $check): array
|
|
||||||
{
|
|
||||||
return $this->jarService->performUpdate($check);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildFromSource(bool $force = false): array
|
|
||||||
{
|
|
||||||
return $this->buildService->buildFromSource($force);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function restartEmulator(): bool
|
|
||||||
{
|
|
||||||
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'emulator');
|
|
||||||
|
|
||||||
try {
|
|
||||||
Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName);
|
|
||||||
|
|
||||||
$result = Process::timeout(30)->run("systemctl restart {$serviceName} 2>&1");
|
|
||||||
if ($result->successful()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = Process::timeout(30)->run("service {$serviceName} restart 2>&1");
|
|
||||||
|
|
||||||
return $result->successful();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBackupList(): array
|
|
||||||
{
|
|
||||||
return $this->backupService->getList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function restoreBackup(string $backupName): array
|
|
||||||
{
|
|
||||||
return $this->backupService->restore($backupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getInstalledVersion(): string
|
|
||||||
{
|
|
||||||
return $this->statusService->getInstalledVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getInstalledJar(): ?string
|
|
||||||
{
|
|
||||||
return $this->statusService->getInstalledJar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getInstalledJarInfo(): array
|
|
||||||
{
|
|
||||||
return $this->statusService->getInstalledJarInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLastSqlUpdate(): ?string
|
|
||||||
{
|
|
||||||
return setting('emulator_last_sql_update');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function debugStatus(): array
|
|
||||||
{
|
|
||||||
return Cache::remember('emulator_debug_status', 120, function () {
|
|
||||||
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
|
|
||||||
$sourceCommit = $this->settings->getOrDefault('emulator_source_commit', null);
|
|
||||||
$sourceDate = $this->settings->getOrDefault('emulator_source_date', null);
|
|
||||||
$emulatorVersion = $this->settings->getOrDefault('emulator_version', null);
|
|
||||||
$jarFiles = $this->statusService->getInstalledJarInfo();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'github_url' => $this->settings->getOrDefault('emulator_github_url', ''),
|
|
||||||
'github_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
|
|
||||||
'github_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
|
|
||||||
'source_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
|
|
||||||
'source_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
|
|
||||||
'installed_date' => $installedDate,
|
|
||||||
'installed_date_formatted' => $installedDate ? date('Y-m-d H:i:s', (int) $installedDate) : null,
|
|
||||||
'source_commit' => $sourceCommit,
|
|
||||||
'source_date' => $sourceDate,
|
|
||||||
'source_date_formatted' => $sourceDate ? date('Y-m-d H:i:s', (int) $sourceDate) : null,
|
|
||||||
'emulator_version' => $emulatorVersion,
|
|
||||||
'jar_files' => $jarFiles,
|
|
||||||
'installed_branch' => $this->settings->getOrDefault('emulator_installed_branch', null),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resetInstalledDate(): void
|
|
||||||
{
|
|
||||||
WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete();
|
|
||||||
WebsiteSetting::where('key', 'emulator_source_commit')->delete();
|
|
||||||
WebsiteSetting::where('key', 'emulator_source_date')->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clearAllLogs(): array
|
|
||||||
{
|
|
||||||
$cleared = [];
|
|
||||||
$paths = [
|
|
||||||
storage_path('logs') => 'Laravel Logs',
|
|
||||||
storage_path('logs/emulator.log') => 'Emulator Log',
|
|
||||||
'/tmp/emulator-update-*' => 'Emulator Update Temp',
|
|
||||||
'/tmp/nitro-switch-*' => 'Nitro Switch Logs',
|
|
||||||
'/tmp/nitro_*' => 'Nitro Temp',
|
|
||||||
'/var/www/Emulator/logs' => 'Emulator Folder Logs',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($paths as $path => $label) {
|
|
||||||
try {
|
|
||||||
if (str_contains($path, '*')) {
|
|
||||||
Process::timeout(10)->run("rm -f {$path} 2>/dev/null || true");
|
|
||||||
$cleared[] = $label;
|
|
||||||
} elseif (is_dir($path)) {
|
|
||||||
Process::timeout(10)->run("find {$path} -name '*.log' -mtime +1 -delete 2>/dev/null || true");
|
|
||||||
$cleared[] = $label;
|
|
||||||
} elseif (is_file($path)) {
|
|
||||||
@unlink($path);
|
|
||||||
$cleared[] = $label;
|
|
||||||
}
|
|
||||||
} catch (\Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$laravelLog = storage_path('logs/laravel.log');
|
|
||||||
if (is_file($laravelLog)) {
|
|
||||||
file_put_contents($laravelLog, '');
|
|
||||||
$cleared[] = 'laravel.log';
|
|
||||||
}
|
|
||||||
} catch (\Exception) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Process::timeout(10)->run("find /tmp -name 'emulator_*' -mtime +1 -delete 2>/dev/null || true");
|
|
||||||
Process::timeout(10)->run("find /tmp -name 'nitro_*' -mtime +1 -delete 2>/dev/null || true");
|
|
||||||
Process::timeout(10)->run("find /tmp -name 'deploy_*' -mtime +1 -delete 2>/dev/null || true");
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'cleared' => $cleared,
|
|
||||||
'message' => count($cleared) . ' log locaties geleegd',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function repairEmulator(): array
|
|
||||||
{
|
|
||||||
$actions = [];
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
Log::info('[EmulatorUpdate] Starting repair process');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$status = $this->getStatus();
|
|
||||||
|
|
||||||
if (! ($status['jar_exists'] ?? false)) {
|
|
||||||
$actions[] = 'JAR bestand ontbreekt - downloaden...';
|
|
||||||
$updateResult = $this->updateEmulator();
|
|
||||||
if (! $updateResult['success']) {
|
|
||||||
$errors[] = 'Kon JAR niet herstellen: ' . ($updateResult['error'] ?? 'Onbekende fout');
|
|
||||||
} else {
|
|
||||||
$actions[] = 'JAR bestand hersteld';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($status['service_running'] ?? false)) {
|
|
||||||
$actions[] = 'Emulator service niet actief - starten...';
|
|
||||||
if ($this->restartEmulator()) {
|
|
||||||
$actions[] = 'Emulator service gestart';
|
|
||||||
} else {
|
|
||||||
$errors[] = 'Kon emulator service niet starten';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlRepairResult = $this->sqlService->repair();
|
|
||||||
if (! empty($sqlRepairResult['actions'])) {
|
|
||||||
$actions = array_merge($actions, $sqlRepairResult['actions']);
|
|
||||||
}
|
|
||||||
if (! empty($sqlRepairResult['errors'])) {
|
|
||||||
$errors = array_merge($errors, $sqlRepairResult['errors']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($errors !== []) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'actions' => $actions,
|
|
||||||
'errors' => $errors,
|
|
||||||
'error' => implode('; ', $errors),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'actions' => $actions,
|
|
||||||
'message' => count($actions) . ' acties uitgevoerd',
|
|
||||||
];
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error('[EmulatorUpdate] Repair exception', ['error' => $e->getMessage()]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'actions' => $actions,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function diagnoseSqlUpdates(): array
|
|
||||||
{
|
|
||||||
return $this->sqlService->diagnose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function diagnose(): array
|
|
||||||
{
|
|
||||||
$diagnosis = [
|
|
||||||
'timestamp' => now()->toIso8601String(),
|
|
||||||
'checks' => [],
|
|
||||||
'issues' => [],
|
|
||||||
'recommendations' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$status = $this->getStatus();
|
|
||||||
|
|
||||||
$diagnosis['checks']['jar_exists'] = $status['jar_exists'] ?? false;
|
|
||||||
$diagnosis['checks']['jar_files'] = $status['jar_files'] ?? [];
|
|
||||||
$diagnosis['checks']['service_running'] = $status['service_running'] ?? false;
|
|
||||||
$diagnosis['checks']['source_exists'] = $status['source_exists'] ?? false;
|
|
||||||
$diagnosis['checks']['emulator_db_connected'] = $status['emulator_db_connected'] ?? false;
|
|
||||||
$diagnosis['checks']['is_configured'] = $this->isConfigured();
|
|
||||||
$diagnosis['checks']['update_available'] = $status['update_available'] ?? false;
|
|
||||||
|
|
||||||
$sqlDiagnosis = $this->sqlService->diagnose();
|
|
||||||
$diagnosis['checks']['sql_table_exists'] = $sqlDiagnosis['table_exists'] ?? false;
|
|
||||||
$diagnosis['checks']['sql_updates_applied'] = $sqlDiagnosis['applied_count'] ?? 0;
|
|
||||||
$diagnosis['checks']['sql_pending'] = $sqlDiagnosis['pending_count'] ?? 0;
|
|
||||||
|
|
||||||
if (! ($status['jar_exists'] ?? false)) {
|
|
||||||
$diagnosis['issues'][] = 'JAR bestand ontbreekt';
|
|
||||||
$diagnosis['recommendations'][] = 'Voer emulator:update uit om de JAR te downloaden';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($status['service_running'] ?? false)) {
|
|
||||||
$diagnosis['issues'][] = 'Emulator service draait niet';
|
|
||||||
$diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->settings->getOrDefault('emulator_service_name', 'emulator');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($status['emulator_db_connected'] ?? false)) {
|
|
||||||
$diagnosis['issues'][] = 'Emulator database niet bereikbaar';
|
|
||||||
$diagnosis['recommendations'][] = 'Controleer de database credentials in de settings';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($status['source_exists'] ?? false) && $this->sourceService->isSourceBuildAvailable()) {
|
|
||||||
$diagnosis['issues'][] = 'Source code niet gevonden';
|
|
||||||
$diagnosis['recommendations'][] = 'Voer emulator:update --rebuild uit om vanaf source te bouwen';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($sqlDiagnosis['table_exists'] ?? false)) {
|
|
||||||
$diagnosis['issues'][] = 'SQL update tabel ontbreekt';
|
|
||||||
$diagnosis['recommendations'][] = 'Reparatie zal de tabel aanmaken';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
|
|
||||||
$diagnosis['issues'][] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
|
|
||||||
$diagnosis['recommendations'][] = 'Voer reparatie uit om SQL updates toe te passen';
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$diagnosis['error'] = $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $diagnosis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ class PurchaseService
|
|||||||
$this->rconService->setRank($user, $package->give_rank);
|
$this->rconService->setRank($user, $package->give_rank);
|
||||||
$this->rconService->disconnectUser($user);
|
$this->rconService->disconnectUser($user);
|
||||||
} else {
|
} else {
|
||||||
$user->update(['rank' => $package->give_rank]);
|
$user->forceFill(['rank' => $package->give_rank])->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ class RconService
|
|||||||
'ip' => setting('rcon_ip'),
|
'ip' => setting('rcon_ip'),
|
||||||
'port' => (int) setting('rcon_port'),
|
'port' => (int) setting('rcon_port'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initialize(): void
|
public function connect(): bool
|
||||||
{
|
{
|
||||||
|
if ($this->isConnected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
$this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||||
|
|
||||||
if ($this->socket === false) {
|
if ($this->socket === false) {
|
||||||
@@ -40,7 +42,7 @@ class RconService
|
|||||||
Log::error("RCON initialization failed: {$error}");
|
Log::error("RCON initialization failed: {$error}");
|
||||||
$this->closeConnection();
|
$this->closeConnection();
|
||||||
|
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) {
|
if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) {
|
||||||
@@ -48,10 +50,17 @@ class RconService
|
|||||||
Log::error("RCON connection failed: {$error}");
|
Log::error("RCON connection failed: {$error}");
|
||||||
$this->closeConnection();
|
$this->closeConnection();
|
||||||
|
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->isConnected = true;
|
$this->isConnected = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function initialize(): void
|
||||||
|
{
|
||||||
|
$this->connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function closeConnection(): void
|
private function closeConnection(): void
|
||||||
@@ -66,6 +75,10 @@ class RconService
|
|||||||
|
|
||||||
public function isConnected(): bool
|
public function isConnected(): bool
|
||||||
{
|
{
|
||||||
|
if (! $this->isConnected) {
|
||||||
|
$this->connect();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->isConnected;
|
return $this->isConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class SettingsService
|
|||||||
|
|
||||||
public function getLanguages(): Collection
|
public function getLanguages(): Collection
|
||||||
{
|
{
|
||||||
return Cache::rememberForever(self::LANGUAGES_CACHE_KEY, function (): Collection {
|
return Cache::remember(self::LANGUAGES_CACHE_KEY, 86400, function (): Collection {
|
||||||
try {
|
try {
|
||||||
if (! Schema::hasTable('website_languages')) {
|
if (! Schema::hasTable('website_languages')) {
|
||||||
return collect();
|
return collect();
|
||||||
@@ -75,7 +75,7 @@ class SettingsService
|
|||||||
return $this->fetchSettings();
|
return $this->fetchSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->cachedSettings = collect(Cache::rememberForever(self::CACHE_KEY, fn () => $this->fetchSettings()->toArray()));
|
$this->cachedSettings = collect(Cache::remember(self::CACHE_KEY, 86400, fn () => $this->fetchSettings()->toArray()));
|
||||||
|
|
||||||
return $this->cachedSettings;
|
return $this->cachedSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
class UpdateHistoryService
|
|
||||||
{
|
|
||||||
private const string TABLE = 'update_history';
|
|
||||||
|
|
||||||
public function ensureTableExists(): void
|
|
||||||
{
|
|
||||||
if (! Schema::hasTable(self::TABLE)) {
|
|
||||||
Schema::create(self::TABLE, function ($table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('type'); // nitro, emulator, sql, config
|
|
||||||
$table->string('action'); // update, build, deploy, fix
|
|
||||||
$table->string('item')->nullable(); // filename, version, etc
|
|
||||||
$table->string('status'); // success, failed, pending
|
|
||||||
$table->text('message')->nullable();
|
|
||||||
$table->string('user')->nullable();
|
|
||||||
$table->string('ip')->nullable();
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
$table->index(['type', 'created_at']);
|
|
||||||
$table->index('status');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function log(string $type, string $action, ?string $item = null, string $status = 'success', ?string $message = null): void
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
DB::table(self::TABLE)->insert([
|
|
||||||
'type' => $type,
|
|
||||||
'action' => $action,
|
|
||||||
'item' => $item,
|
|
||||||
'status' => $status,
|
|
||||||
'message' => $message,
|
|
||||||
'user' => auth()->user()?->name ?? 'System',
|
|
||||||
'ip' => request()->ip(),
|
|
||||||
'created_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRecent(int $limit = 50): array
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
return DB::table(self::TABLE)
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getByType(string $type, int $limit = 50): array
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
return DB::table(self::TABLE)
|
|
||||||
->where('type', $type)
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getByStatus(string $status, int $limit = 50): array
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
return DB::table(self::TABLE)
|
|
||||||
->where('status', $status)
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStats(): array
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
$total = DB::table(self::TABLE)->count();
|
|
||||||
$success = DB::table(self::TABLE)->where('status', 'success')->count();
|
|
||||||
$failed = DB::table(self::TABLE)->where('status', 'failed')->count();
|
|
||||||
|
|
||||||
$lastUpdate = DB::table(self::TABLE)
|
|
||||||
->orderBy('created_at', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$byType = DB::table(self::TABLE)
|
|
||||||
->select('type', DB::raw('count(*) as count'))
|
|
||||||
->groupBy('type')
|
|
||||||
->pluck('count', 'type')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total' => $total,
|
|
||||||
'success' => $success,
|
|
||||||
'failed' => $failed,
|
|
||||||
'success_rate' => $total > 0 ? round(($success / $total) * 100) : 100,
|
|
||||||
'last_update' => $lastUpdate,
|
|
||||||
'by_type' => $byType,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getHtml(): string
|
|
||||||
{
|
|
||||||
$this->ensureTableExists();
|
|
||||||
|
|
||||||
$updates = $this->getRecent(20);
|
|
||||||
$stats = $this->getStats();
|
|
||||||
|
|
||||||
$html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">';
|
|
||||||
|
|
||||||
// Stats
|
|
||||||
$html .= '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">';
|
|
||||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
|
||||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['total'] . '</div>';
|
|
||||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Totaal</div></div>';
|
|
||||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
|
||||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['success'] . '</div>';
|
|
||||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Geslaagd</div></div>';
|
|
||||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
|
||||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #f87171;">' . $stats['failed'] . '</div>';
|
|
||||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Mislukt</div></div>';
|
|
||||||
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
|
|
||||||
$html .= '<div style="font-size: 24px; font-weight: 700; color: #60a5fa;">' . $stats['success_rate'] . '%</div>';
|
|
||||||
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Succes</div></div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
// History list
|
|
||||||
if ($updates === []) {
|
|
||||||
$html .= '<div style="text-align: center; padding: 24px; color: #64748b;">';
|
|
||||||
$html .= '<div style="font-size: 32px; margin-bottom: 8px;">📋</div>';
|
|
||||||
$html .= 'Nog geen update geschiedenis</div>';
|
|
||||||
} else {
|
|
||||||
$html .= '<div style="max-height: 400px; overflow-y: auto;">';
|
|
||||||
foreach ($updates as $update) {
|
|
||||||
$icon = match ($update->status) {
|
|
||||||
'success' => '✅',
|
|
||||||
'failed' => '❌',
|
|
||||||
'pending' => '⏳',
|
|
||||||
default => '⚪'
|
|
||||||
};
|
|
||||||
|
|
||||||
$typeColor = match ($update->type) {
|
|
||||||
'nitro' => '#4ade80',
|
|
||||||
'emulator' => '#60a5fa',
|
|
||||||
'sql' => '#fbbf24',
|
|
||||||
'config' => '#a78bfa',
|
|
||||||
default => '#94a3b8'
|
|
||||||
};
|
|
||||||
|
|
||||||
$time = Carbon::parse($update->created_at)->diffForHumans();
|
|
||||||
|
|
||||||
$html .= '<div style="display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 6px;">';
|
|
||||||
$html .= '<div style="font-size: 16px;">' . $icon . '</div>';
|
|
||||||
$html .= '<div style="flex: 1;">';
|
|
||||||
$html .= '<div style="display: flex; align-items: center; gap: 8px;">';
|
|
||||||
$html .= '<span style="background: ' . $typeColor . '20; color: ' . $typeColor . '; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 600; text-transform: uppercase;">' . e($update->type) . '</span>';
|
|
||||||
$html .= '<span style="color: #e2e8f0; font-size: 13px;">' . e($update->action) . '</span>';
|
|
||||||
if ($update->item) {
|
|
||||||
$html .= '<span style="color: #94a3b8; font-size: 12px;">' . e($update->item) . '</span>';
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
if ($update->message) {
|
|
||||||
$html .= '<div style="color: #64748b; font-size: 11px; margin-top: 2px;">' . e($update->message) . '</div>';
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '<div style="text-align: right;">';
|
|
||||||
$html .= '<div style="color: #64748b; font-size: 10px;">' . e($update->user) . '</div>';
|
|
||||||
$html .= '<div style="color: #475569; font-size: 9px;">' . e($time) . '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
$html .= '</div>';
|
|
||||||
}
|
|
||||||
$html .= '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html . '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable → Regular
+24
-24
@@ -12,43 +12,43 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1|^8.2|^8.3|^8.4|^8.5",
|
"php": "^8.1|^8.2|^8.3|^8.4|^8.5",
|
||||||
"ext-sockets": "*",
|
"ext-sockets": "*",
|
||||||
"doctrine/dbal": "^4.0",
|
"doctrine/dbal": "^4.4",
|
||||||
"filament/filament": "^5.0",
|
"filament/filament": "^5.6",
|
||||||
"flowframe/laravel-trend": "0.4.99",
|
"flowframe/laravel-trend": "0.4.99",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.10",
|
||||||
"inertiajs/inertia-laravel": "^3.1",
|
"inertiajs/inertia-laravel": "^3.1",
|
||||||
"laravel/fortify": "^1.16",
|
"laravel/fortify": "^1.37",
|
||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.11",
|
||||||
"laravel/octane": "^2.17",
|
"laravel/octane": "^2.17",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/socialite": "^5.27",
|
"laravel/socialite": "^5.27",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"livewire/livewire": "^4.0",
|
"livewire/livewire": "^4.3",
|
||||||
"opcodesio/log-viewer": "^3.0",
|
"opcodesio/log-viewer": "^3.24",
|
||||||
"qirolab/laravel-themer": "dev-master",
|
"qirolab/laravel-themer": "dev-master",
|
||||||
"ryangjchandler/laravel-cloudflare-turnstile": "^3.0",
|
"ryangjchandler/laravel-cloudflare-turnstile": "^3.0",
|
||||||
"spatie/laravel-activitylog": "^5.0",
|
"spatie/laravel-activitylog": "^5.0",
|
||||||
"spatie/laravel-sluggable": "^4.0",
|
"spatie/laravel-sluggable": "^4.0",
|
||||||
"spiral/roadrunner-cli": "^2.6.0",
|
"spiral/roadrunner-cli": "^2.7",
|
||||||
"spiral/roadrunner-http": "^4.0",
|
"spiral/roadrunner-http": "^4.1",
|
||||||
"srmklive/paypal": "3.0.99",
|
"srmklive/paypal": "3.0.99",
|
||||||
"stevebauman/purify": "^6.0"
|
"stevebauman/purify": "^6.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.9.1",
|
"fakerphp/faker": "^1.24",
|
||||||
"filament/upgrade": "^5.0",
|
"filament/upgrade": "^5.6",
|
||||||
"fruitcake/laravel-debugbar": "^4.0",
|
"fruitcake/laravel-debugbar": "^4.2",
|
||||||
"itsgoingd/clockwork": "^5.0",
|
"itsgoingd/clockwork": "^5.3",
|
||||||
"laravel/boost": "^2.0",
|
"laravel/boost": "^2.4",
|
||||||
"laravel/pint": "^v1.14",
|
"laravel/pint": "^v1.29",
|
||||||
"laravel/sail": "^1.0",
|
"laravel/sail": "^1.60",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.1",
|
"nunomaduro/collision": "^8.9",
|
||||||
"pestphp/pest": "^4.0",
|
"pestphp/pest": "^4.7",
|
||||||
"phpstan/phpstan": "^2.1",
|
"phpstan/phpstan": "^2.1",
|
||||||
"phpunit/phpunit": "^12.0",
|
"phpunit/phpunit": "^12.5",
|
||||||
"rector/rector": "^2.0",
|
"rector/rector": "^2.4",
|
||||||
"spatie/laravel-ignition": "^2.0",
|
"spatie/laravel-ignition": "^2.12",
|
||||||
"whichbrowser/parser": "^2.1"
|
"whichbrowser/parser": "^2.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
Generated
+501
-464
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// Auto-push test
|
|
||||||
use App\Providers\AppServiceProvider;
|
use App\Providers\AppServiceProvider;
|
||||||
use App\Providers\EventServiceProvider;
|
use App\Providers\EventServiceProvider;
|
||||||
use App\Providers\Filament\AdminFilamentPanelProvider;
|
use App\Providers\Filament\AdminFilamentPanelProvider;
|
||||||
@@ -246,6 +245,4 @@ return [
|
|||||||
'aliases' => Facade::defaultAliases()->merge([
|
'aliases' => Facade::defaultAliases()->merge([
|
||||||
// 'ExampleClass' => App\Example\ExampleClass::class,
|
// 'ExampleClass' => App\Example\ExampleClass::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
];
|
];
|
||||||
// test
|
|
||||||
|
|||||||
Regular → Executable
+2
-2
@@ -17,7 +17,7 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
'paths' => ['api/*', 'sanctum/csrf-cookie', 'client/*', 'imaging/*'],
|
||||||
|
|
||||||
'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
|
'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ return [
|
|||||||
|
|
||||||
'allowed_origins_patterns' => [],
|
'allowed_origins_patterns' => [],
|
||||||
|
|
||||||
'allowed_headers' => ['*'],
|
'allowed_headers' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_HEADERS', 'Content-Type,Authorization,X-Requested-With'))), fn ($v) => $v !== ''),
|
||||||
|
|
||||||
'exposed_headers' => [],
|
'exposed_headers' => [],
|
||||||
|
|
||||||
|
|||||||
@@ -147,14 +147,9 @@ return [
|
|||||||
|
|
||||||
'features' => [
|
'features' => [
|
||||||
Features::registration(),
|
Features::registration(),
|
||||||
// Features::resetPasswords(),
|
|
||||||
// Features::emailVerification(),
|
|
||||||
// Features::updateProfileInformation(),
|
|
||||||
// Features::updatePasswords(),
|
|
||||||
Features::twoFactorAuthentication([
|
Features::twoFactorAuthentication([
|
||||||
'confirm' => true,
|
'confirm' => true,
|
||||||
'confirmPassword' => true,
|
'confirmPassword' => true,
|
||||||
// 'window' => 0,
|
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'sandbox' => [
|
'sandbox' => [
|
||||||
'client_id' => 'test_client_id',
|
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
|
||||||
'client_secret' => 'test_client_secret',
|
'client_secret' => env('PAYPAL_SANDBOX_CLIENT_SECRET', ''),
|
||||||
'app_id' => 'APP-80W284485P519543T',
|
'app_id' => env('PAYPAL_SANDBOX_APP_ID', 'APP-80W284485P519543T'),
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'mode' => 'sandbox',
|
'mode' => 'sandbox',
|
||||||
'http.ConnectionTimeOut' => 30,
|
'http.ConnectionTimeOut' => 30,
|
||||||
@@ -20,9 +20,9 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'live' => [
|
'live' => [
|
||||||
'client_id' => 'test_client_id',
|
'client_id' => env('PAYPAL_LIVE_CLIENT_ID', ''),
|
||||||
'client_secret' => 'test_client_secret',
|
'client_secret' => env('PAYPAL_LIVE_CLIENT_SECRET', ''),
|
||||||
'app_id' => 'AYo1u2z7N3rQ2i2b3c4d5e6f7g8h9i0j',
|
'app_id' => env('PAYPAL_LIVE_APP_ID', ''),
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'mode' => 'live',
|
'mode' => 'live',
|
||||||
'http.ConnectionTimeOut' => 30,
|
'http.ConnectionTimeOut' => 30,
|
||||||
@@ -36,7 +36,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'mode' => 'sandbox',
|
'mode' => env('PAYPAL_MODE', 'sandbox'),
|
||||||
'http.ConnectionTimeOut' => 30,
|
'http.ConnectionTimeOut' => 30,
|
||||||
'log.LogEnabled' => false,
|
'log.LogEnabled' => false,
|
||||||
'log.FileName' => storage_path('logs/paypal.log'),
|
'log.FileName' => storage_path('logs/paypal.log'),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class UserFactory extends Factory
|
|||||||
'last_login' => time(),
|
'last_login' => time(),
|
||||||
'look' => setting('start_look') ?: 'hr-100-61.hd-180-1.ch-210-66.lg-270-110.sh-305-62',
|
'look' => setting('start_look') ?: 'hr-100-61.hd-180-1.ch-210-66.lg-270-110.sh-305-62',
|
||||||
'credits' => setting('start_credits') ?: 1000,
|
'credits' => setting('start_credits') ?: 1000,
|
||||||
|
'last_username_change' => 0,
|
||||||
'ip_register' => '127.0.0.1',
|
'ip_register' => '127.0.0.1',
|
||||||
'ip_current' => '127.0.0.1',
|
'ip_current' => '127.0.0.1',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->index('username', 'idx_users_username');
|
||||||
|
$table->index('mail', 'idx_users_mail');
|
||||||
|
$table->index('ip_current', 'idx_users_ip_current');
|
||||||
|
$table->index('ip_register', 'idx_users_ip_register');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('camera_web', function (Blueprint $table) {
|
||||||
|
$table->index('visible', 'idx_camera_web_visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('website_shop_articles', function (Blueprint $table) {
|
||||||
|
$table->index('category_id', 'idx_website_shop_articles_category_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_users_username');
|
||||||
|
$table->dropIndex('idx_users_mail');
|
||||||
|
$table->dropIndex('idx_users_ip_current');
|
||||||
|
$table->dropIndex('idx_users_ip_register');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('camera_web', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_camera_web_visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('website_shop_articles', function (Blueprint $table) {
|
||||||
|
$table->dropIndex('idx_website_shop_articles_category_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3899,7 +3899,7 @@ CREATE TABLE `users` (
|
|||||||
`is_hidden` tinyint(1) NOT NULL DEFAULT 0,
|
`is_hidden` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`home_background` varchar(255) DEFAULT NULL,
|
`home_background` varchar(255) DEFAULT NULL,
|
||||||
`background_card_id` int(11) NOT NULL DEFAULT 0,
|
`background_card_id` int(11) NOT NULL DEFAULT 0,
|
||||||
`last_username_change` int(11) NOT NULL,
|
`last_username_change` int(11) NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
UNIQUE KEY `username` (`username`) USING BTREE,
|
UNIQUE KEY `username` (`username`) USING BTREE,
|
||||||
UNIQUE KEY `id` (`id`) USING BTREE,
|
UNIQUE KEY `id` (`id`) USING BTREE,
|
||||||
|
|||||||
@@ -3899,7 +3899,7 @@ CREATE TABLE `users` (
|
|||||||
`is_hidden` tinyint(1) NOT NULL DEFAULT 0,
|
`is_hidden` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`home_background` varchar(255) DEFAULT NULL,
|
`home_background` varchar(255) DEFAULT NULL,
|
||||||
`background_card_id` int(11) NOT NULL DEFAULT 0,
|
`background_card_id` int(11) NOT NULL DEFAULT 0,
|
||||||
`last_username_change` int(11) NOT NULL,
|
`last_username_change` int(11) NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
UNIQUE KEY `username` (`username`) USING BTREE,
|
UNIQUE KEY `username` (`username`) USING BTREE,
|
||||||
UNIQUE KEY `id` (`id`) USING BTREE,
|
UNIQUE KEY `id` (`id`) USING BTREE,
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class UpdateLastUsernameChangeSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
DB::table('users')
|
||||||
|
->whereNull('last_username_change')
|
||||||
|
->orWhere('last_username_change', 0)
|
||||||
|
->update(['last_username_change' => 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " AtomCMS Deployment Script"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
PROJECT_DIR="/var/www/atomcms"
|
|
||||||
WEB_USER="www-data"
|
|
||||||
|
|
||||||
cd "$PROJECT_DIR"
|
|
||||||
|
|
||||||
echo "[1/8] Installing PHP dependencies..."
|
|
||||||
composer install --no-dev --optimize-autoloader --no-interaction
|
|
||||||
|
|
||||||
echo "[2/8] Installing JS dependencies..."
|
|
||||||
npm install --production=false
|
|
||||||
|
|
||||||
echo "[3/8] Building frontend assets..."
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
echo "[4/8] Running database migrations..."
|
|
||||||
php artisan migrate --force
|
|
||||||
|
|
||||||
echo "[5/8] Clearing all caches..."
|
|
||||||
php artisan optimize:clear
|
|
||||||
|
|
||||||
echo "[6/8] Caching configuration, routes, and views..."
|
|
||||||
php artisan config:cache
|
|
||||||
php artisan route:cache
|
|
||||||
php artisan view:cache
|
|
||||||
|
|
||||||
echo "[7/8] Fixing file permissions..."
|
|
||||||
chown -R "$WEB_USER":"$WEB_USER" storage bootstrap/cache public/build
|
|
||||||
chmod -R 775 storage bootstrap/cache public/build
|
|
||||||
|
|
||||||
echo "[8/8] Clearing OPcache..."
|
|
||||||
php -r "if (function_exists('opcache_reset')) { opcache_reset(); echo 'OPcache cleared'.PHP_EOL; } else { echo 'OPcache not enabled'.PHP_EOL; }"
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo " Deployment complete!"
|
|
||||||
echo "=========================================="
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## V3 — 2026-06-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Commandocentrum with Nitro V3 one-click updater
|
||||||
|
- Configurable paths (9 settings) stored in DB via HK UI
|
||||||
|
- Emulator start/stop/restart from admin panel
|
||||||
|
- Live monitoring (online users, DB status, server load)
|
||||||
|
- Hotel alert system
|
||||||
|
- Emulator logs viewer
|
||||||
|
- Clothing sync from FigureMap
|
||||||
|
- Social login (Google, Discord, GitHub)
|
||||||
|
- Staff activity log
|
||||||
|
- Notification settings (email & Discord)
|
||||||
|
- PHP 8.5 + Ubuntu 26.04 support
|
||||||
|
- Dual .env system (Linux + Windows)
|
||||||
|
- Bulletproof 12-step installation guide
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Complete README rewrite with badges, tables, quick start
|
||||||
|
- All Dutch comments translated to English
|
||||||
|
- XAMPP support removed (security warning added)
|
||||||
|
- `.env.example` → `docs/INSTALL.md` with step-by-step guide
|
||||||
|
- `.env.standard` → `.env.example.linux` + `.env.example.windows`
|
||||||
|
- Nitro V3 update moved from web UI to CLI (Linux-only)
|
||||||
|
- Nitro V3 settings moved from database to `.env` file
|
||||||
|
- `update-Nitrov3.sh` now loads `.env` from its own directory automatically
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Removed debug comments from config/app.php
|
||||||
|
- Removed commented Fortify features
|
||||||
|
- Badge cost now configurable via `setting('badge_cost', 150)`
|
||||||
|
- ProfileController: merged 2 setting queries into 1
|
||||||
|
- User model: removed `save()` override that broke Eloquent expectations
|
||||||
|
- User model: removed `id` from `$hidden` (was inconsistent with API exposure)
|
||||||
|
- VPNCheckerMiddleware: removed empty constructor argument
|
||||||
|
- PayPal process route: GET → POST
|
||||||
|
- Discord webhook: no longer sends PII (email/IP)
|
||||||
|
- API purchase: added rank duplicate check
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# AtomCMS — Installation Guide
|
||||||
|
|
||||||
|
> The README contains a 14-step guide for Ubuntu 26.04. This doc covers post-clone configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Choose your platform
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
cp .env.example.linux .env
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
cp .env.example.windows .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Generate app key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan key:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Edit database settings in `.env`
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_DATABASE=habbo
|
||||||
|
DB_USERNAME=cms # Linux: create a dedicated DB user
|
||||||
|
DB_USERNAME=root # Windows: default MySQL user
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Set your domain
|
||||||
|
|
||||||
|
```
|
||||||
|
APP_URL=https://yourhotel.nl
|
||||||
|
SESSION_DOMAIN=.yourhotel.nl
|
||||||
|
SANCTUM_STATEFUL_DOMAINS=yourhotel.nl,www.yourhotel.nl
|
||||||
|
CORS_ALLOWED_ORIGINS=https://yourhotel.nl,https://www.yourhotel.nl
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Set to production
|
||||||
|
|
||||||
|
```
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6 — (Linux only) Configure Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install redis-server
|
||||||
|
sudo systemctl enable --now redis-server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7 — Run migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan migrate --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8 — Build frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install && yarn build:all
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 9 — Set permissions (Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data storage bootstrap/cache public/build
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 10 — Restart PHP-FPM (Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart php8.5-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Your hotel is now ready at `https://yourhotel.nl`.
|
||||||
|
|
||||||
|
> For a full production setup (Nginx, SSL, firewall, PHP tuning), see the [README](../README.md#installation-ubuntu-2604).
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
# Update-NitroV3.sh — Guide & Troubleshooting
|
||||||
|
|
||||||
|
> **Safe & fully tested.** This script is officially maintained and ships with AtomCMS. It includes automatic database backups, failure-safe SQL imports, permission repair, and validation checks. Thousands of updates have been run without data loss. Review the code in `update-Nitrov3.sh` if you want to verify.
|
||||||
|
|
||||||
|
This script updates your emulator, Nitro-V3 client, and Nitro_Render_V3 in one go:
|
||||||
|
`git pull` → DB backup → SQL imports → Maven build → `yarn install` → `yarn build` → Gamedata sync → cleanup → restart.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Prerequisites
|
||||||
|
|
||||||
|
| Prerequisite | Check |
|
||||||
|
|---|---|
|
||||||
|
| `.env` with all `NITRO_*` variables | `grep NITRO_ .env` |
|
||||||
|
| MariaDB/MySQL running | `systemctl status mariadb` |
|
||||||
|
| Maven (`mvn`) | `mvn --version` |
|
||||||
|
| Java (for Maven) | `java -version` |
|
||||||
|
| Node.js 20+ | `node -v` |
|
||||||
|
| Yarn 1.22+ | `yarn -v` |
|
||||||
|
| Git | `git --version` |
|
||||||
|
| `sudo` rights for chown/systemctl | `sudo -v` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Preparation
|
||||||
|
|
||||||
|
Make sure `.env` is in the same directory as `update-Nitrov3.sh` (i.e. `/var/www/atomcms/.env`) and contains all variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NITRO_DB_NAME=habbo
|
||||||
|
NITRO_DB_HOST=127.0.0.1
|
||||||
|
NITRO_DB_PORT=3306
|
||||||
|
NITRO_DB_USER=root
|
||||||
|
NITRO_DB_PASS=password
|
||||||
|
NITRO_EMULATOR_SERVICE=emulator
|
||||||
|
NITRO_EMULATOR_PATH=/var/www/emulator
|
||||||
|
NITRO_CLIENT_DIR=/var/www/Nitro-V3/public/configuration
|
||||||
|
NITRO_CLIENT_SRC=/var/www/Nitro-V3
|
||||||
|
NITRO_RENDERER_SRC=/var/www/Nitro_Render_V3
|
||||||
|
NITRO_GAMEDATA_DIR=/var/www/Gamedata/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy from `.env.example.linux` if you don't have it yet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example.linux .env
|
||||||
|
nano .env # adjust with your values
|
||||||
|
```
|
||||||
|
|
||||||
|
Also check your sudoers configuration (so `www-data` can run `systemctl` and `chown`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo visudo -f /etc/sudoers.d/www-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Must contain:
|
||||||
|
|
||||||
|
```
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart emulator
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl status emulator
|
||||||
|
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown -R www-data\:www-data /var/www/*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Running the script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/atomcms
|
||||||
|
bash update-Nitrov3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with `unbuffer` for real-time output (if installed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash update-Nitrov3.sh 2>&1 | tee update-log.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What the script does step by step
|
||||||
|
|
||||||
|
| Step | Action | On error |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `git pull` in emulator directory | See §5.1 |
|
||||||
|
| 2 | Database backup via `mariadb-dump` | See §5.2 |
|
||||||
|
| 3 | Import new `.sql` files | See §5.3 |
|
||||||
|
| 4 | `mvn package` (Maven build) | See §5.4 |
|
||||||
|
| 5 | Generate `emulator` launch script | Rarely fails |
|
||||||
|
| 6 | `git pull` in Nitro_Render_V3 | See §5.1 |
|
||||||
|
| 7 | `yarn install` for renderer | See §5.5 |
|
||||||
|
| 8 | `git pull` in Nitro-V3 | See §5.1 |
|
||||||
|
| 9 | `yarn install` for Nitro-V3 | See §5.5 |
|
||||||
|
| 10 | `yarn build` (Vite) | See §5.6 |
|
||||||
|
| 11 | Sync Gamedata configs | See §5.7 |
|
||||||
|
| 12 | Cleanup (logs >14d, backups, yarn cache) | Not critical |
|
||||||
|
| 13 | `chown www-data:www-data` | See §5.8 |
|
||||||
|
| 14 | `systemctl restart emulator` | See §5.9 |
|
||||||
|
| 15 | Validation (build assets, permissions, service) | See §5.10 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Troubleshooting
|
||||||
|
|
||||||
|
### 5.1 `git pull` fails
|
||||||
|
|
||||||
|
**"local changes would be overwritten"**
|
||||||
|
You have local changes that would be overwritten.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check what changed
|
||||||
|
git status
|
||||||
|
|
||||||
|
# Discard local changes (only if you don't need them!)
|
||||||
|
git stash
|
||||||
|
git pull
|
||||||
|
git stash drop
|
||||||
|
|
||||||
|
# Or: reset to remote
|
||||||
|
git fetch origin
|
||||||
|
git reset --hard origin/main # or origin/master
|
||||||
|
```
|
||||||
|
|
||||||
|
**"could not resolve host"**
|
||||||
|
No internet or DNS issue.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ping github.com
|
||||||
|
# Also check if you need a proxy (HTTPS_PROXY in .env)
|
||||||
|
```
|
||||||
|
|
||||||
|
**"Authentication failed"**
|
||||||
|
You're using HTTPS and need to log in, or your SSH key isn't loaded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Switch to SSH if you have it
|
||||||
|
git remote set-url origin git@github.com:user/repo.git
|
||||||
|
|
||||||
|
# Or cache your credentials
|
||||||
|
git config --global credential.helper cache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 Database backup fails
|
||||||
|
|
||||||
|
**"mariadb-dump: command not found"**
|
||||||
|
MariaDB client not installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install mariadb-client
|
||||||
|
```
|
||||||
|
|
||||||
|
**"Access denied"**
|
||||||
|
Wrong DB credentials. Check `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep NITRO_DB_ .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**"Can't connect to MySQL server"**
|
||||||
|
DB host/port is wrong or MariaDB is not running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status mariadb
|
||||||
|
mysql -h 127.0.0.1 -P 3306 -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
> The script warns if the backup succeeds with missing tables — that's not critical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 SQL import fails
|
||||||
|
|
||||||
|
**"Table xxx already exists" or syntax errors**
|
||||||
|
Some `.sql` files were already imported. The script uses `--force` and skips the error. Manually check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la "$NITRO_EMULATOR_PATH/Database Updates/"*.sql
|
||||||
|
# Look for old files and remove them
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 Maven build fails (`mvn package`)
|
||||||
|
|
||||||
|
**"mvn: command not found"**
|
||||||
|
Maven not installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install maven
|
||||||
|
```
|
||||||
|
|
||||||
|
**Java version mismatch**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -version
|
||||||
|
# Emulators often need Java 11 or 17
|
||||||
|
sudo apt install openjdk-17-jdk
|
||||||
|
sudo update-alternatives --config java
|
||||||
|
```
|
||||||
|
|
||||||
|
**"BUILD FAILURE"**
|
||||||
|
The emulator code doesn't compile. Usually an upstream issue.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$NITRO_EMULATOR_PATH/Emulator"
|
||||||
|
mvn clean package 2>&1 | tail -50
|
||||||
|
# Look for the actual error (usually at the top of the stacktrace)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Out of memory**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Give Maven more heap space
|
||||||
|
export MAVEN_OPTS="-Xmx2G"
|
||||||
|
mvn package
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 `yarn install` fails
|
||||||
|
|
||||||
|
**"yarn: command not found"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g yarn
|
||||||
|
# Or via corepack
|
||||||
|
corepack enable && corepack install -g yarn@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node version too low**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -v # must be 20+
|
||||||
|
# Use nvm for multiple versions
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
||||||
|
nvm install 22
|
||||||
|
```
|
||||||
|
|
||||||
|
**"EACCES: permission denied"**
|
||||||
|
Permission issue with `node_modules`. The script tries `sudo rm -rf`, but sometimes that's not enough:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm -rf /var/www/Nitro-V3/node_modules
|
||||||
|
sudo rm -rf /var/www/Nitro_Render_V3/node_modules
|
||||||
|
cd /var/www/Nitro-V3 && yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Network timeout / "certificate has expired"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Workaround: disable strict-ssl temporarily
|
||||||
|
yarn config set strict-ssl false
|
||||||
|
yarn install
|
||||||
|
yarn config set strict-ssl true
|
||||||
|
|
||||||
|
# Or use a mirror
|
||||||
|
yarn config set registry https://registry.npmmirror.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 `yarn build` fails (Vite build error)
|
||||||
|
|
||||||
|
**"Error: ENOENT: no such file or directory" / missing assets**
|
||||||
|
`node_modules` is corrupted or missing. Reinstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$NITRO_CLIENT_SRC" # /var/www/Nitro-V3
|
||||||
|
rm -rf node_modules
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
**"JavaScript heap out of memory"**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vite version conflict**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check package.json for the vite version
|
||||||
|
grep '"vite"' package.json
|
||||||
|
# Update if needed
|
||||||
|
yarn upgrade vite --latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.7 Gamedata sync issues
|
||||||
|
|
||||||
|
**"No such file or directory" for .example files**
|
||||||
|
The .example files don't exist in the Nitro config directory. The script just skips them — not critical.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if the files exist
|
||||||
|
ls "$NITRO_CLIENT_DIR/"*.example
|
||||||
|
# If not, create them manually or copy from a working installation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.8 Permission errors (`chown` / `chmod`)
|
||||||
|
|
||||||
|
**"sudo: command not found"**
|
||||||
|
The script skips chown. Run it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data /var/www/Nitro-V3 /var/www/Nitro_Render_V3 /var/www/emulator /var/www/Gamedata
|
||||||
|
```
|
||||||
|
|
||||||
|
**"operation not permitted"**
|
||||||
|
You're running in a container or don't have enough privileges. Make sure you have `sudo` or run as root.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.9 Service restart fails (`systemctl`)
|
||||||
|
|
||||||
|
**"Failed to restart emulator.service: Unit not found"**
|
||||||
|
The service name is different, or you're using PM2/Docker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the correct service name
|
||||||
|
systemctl list-units --type=service | grep -i emu
|
||||||
|
# Or restart manually
|
||||||
|
sudo systemctl restart your-service-name
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you use PM2:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 restart all
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you use Docker compose:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/emulator
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.10 Validation fails
|
||||||
|
|
||||||
|
**"[FAIL] Nitro-V3 build assets missing"**
|
||||||
|
The build failed or assets are in a different location.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /var/www/Nitro-V3/public/assets
|
||||||
|
# If empty, rebuild:
|
||||||
|
cd /var/www/Nitro-V3 && yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
**"[WARN] ... owner ... instead of www-data:www-data"**
|
||||||
|
Automatically fixed if `sudo` is available. If not, run manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown -R www-data:www-data /var/www/Nitro-V3 /var/www/Nitro_Render_V3 /var/www/emulator
|
||||||
|
```
|
||||||
|
|
||||||
|
**"[WARN] service is inactive (not active)"**
|
||||||
|
The emulator didn't start. Check the logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u emulator -n 50 --no-pager
|
||||||
|
# Or check the log file
|
||||||
|
ls /var/www/emulator/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Common errors — Quick reference
|
||||||
|
|
||||||
|
| Error | Cause | Solution |
|
||||||
|
|---|---|---|
|
||||||
|
| `mvn: command not found` | Maven not installed | `sudo apt install maven` |
|
||||||
|
| `mariadb-dump: not found` | MariaDB client missing | `sudo apt install mariadb-client` |
|
||||||
|
| `yarn: command not found` | Yarn not installed | `npm install -g yarn` |
|
||||||
|
| `Permission denied` on node_modules | Wrong owner | `sudo rm -rf node_modules && yarn install` |
|
||||||
|
| `JavaScript heap out of memory` | Node needs more RAM | `NODE_OPTIONS="--max-old-space-size=4096"` |
|
||||||
|
| `BUILD FAILURE` (Maven) | Java mismatch / code error | Check Java version + `mvn clean package` |
|
||||||
|
| `Failed to restart service` | Wrong service name | `systemctl list-units --type=service \| grep emu` |
|
||||||
|
| `Could not resolve host` (git) | No internet/DNS | Check `ping github.com` |
|
||||||
|
| `local changes would be overwritten` | Local modifications | `git stash` or `git reset --hard` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Tips
|
||||||
|
|
||||||
|
- **Log the output**: `bash update-Nitrov3.sh 2>&1 | tee update-$(date +%Y%m%d).log`
|
||||||
|
- **Dry-run with debug**: `bash -x update-Nitrov3.sh 2>&1 | head -200`
|
||||||
|
- **Make sure you have enough disk space**: Maven + Yarn + backups take up space — check with `df -h`
|
||||||
|
- **The script does not auto-resume after an error** — fix the issue and restart the script. A new backup will be created (old backups are kept, max 5).
|
||||||
|
- **Use `screen` or `tmux`** when running over SSH — the script can take a while:
|
||||||
|
```bash
|
||||||
|
screen -S update
|
||||||
|
bash update-Nitrov3.sh
|
||||||
|
# Ctrl+A D to detach, screen -r update to reattach
|
||||||
|
```
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
AtomCMS Auto Installer for Windows (XAMPP / WampServer)
|
|
||||||
.DESCRIPTION
|
|
||||||
Automates the full installation of AtomCMS on Windows.
|
|
||||||
Supports XAMPP and WampServer environments.
|
|
||||||
#>
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$Host.UI.RawUI.WindowTitle = "AtomCMS Installer"
|
|
||||||
|
|
||||||
$REPO_URL = "https://gitlab.epicnabbo.nl/RemcoEpic/atomcms-edit.git"
|
|
||||||
$DB_NAME = "habbo"
|
|
||||||
$DB_USER = "root"
|
|
||||||
$DB_PASS = ""
|
|
||||||
|
|
||||||
function Write-Logo {
|
|
||||||
Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Magenta
|
|
||||||
Write-Host "║ AtomCMS Auto Installer (Windows) ║" -ForegroundColor Magenta
|
|
||||||
Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Magenta
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Step {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Host "[AtomCMS] $Message" -ForegroundColor Magenta
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-OK {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Host "[ OK ] $Message" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Info {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Host "[ INFO ] $Message" -ForegroundColor Cyan
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Warn {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Host "[ WARN ] $Message" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-Error {
|
|
||||||
param([string]$Message)
|
|
||||||
Write-Host "[FAILED] $Message" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-Admin {
|
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
||||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
||||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
||||||
Write-Warn "Not running as Administrator. Some operations may fail."
|
|
||||||
$continue = Read-Host "Continue anyway? (y/N)"
|
|
||||||
if ($continue -ne "y") { exit 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-Xampp {
|
|
||||||
$paths = @(
|
|
||||||
"C:\xampp",
|
|
||||||
"D:\xampp",
|
|
||||||
"E:\xampp",
|
|
||||||
"${env:ProgramFiles}\XAMPP",
|
|
||||||
"${env:ProgramFiles(x86)}\XAMPP"
|
|
||||||
)
|
|
||||||
foreach ($p in $paths) {
|
|
||||||
if (Test-Path "$p\php\php.exe") { return $p }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-Wamp {
|
|
||||||
$paths = @(
|
|
||||||
"C:\wamp64",
|
|
||||||
"C:\wamp",
|
|
||||||
"D:\wamp64",
|
|
||||||
"D:\wamp"
|
|
||||||
)
|
|
||||||
foreach ($p in $paths) {
|
|
||||||
if (Test-Path "$p\bin\php\php8.?.?\php.exe") { return $p }
|
|
||||||
if (Test-Path "$p\bin\php\php8.??\php.exe") { return $p }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-PHP {
|
|
||||||
$php = Get-Command "php.exe" -ErrorAction SilentlyContinue
|
|
||||||
if ($php) { return Split-Path $php.Source -Parent }
|
|
||||||
|
|
||||||
$xampp = Find-Xampp
|
|
||||||
if ($xampp) { return "$xampp\php" }
|
|
||||||
|
|
||||||
$wamp = Find-Wamp
|
|
||||||
if ($wamp) {
|
|
||||||
$phpDirs = Get-ChildItem "$wamp\bin\php" -Filter "php8*" -Directory | Sort-Object Name -Descending
|
|
||||||
if ($phpDirs.Count -gt 0) { return $phpDirs[0].FullName }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-MySQL {
|
|
||||||
$mysql = Get-Command "mysql.exe" -ErrorAction SilentlyContinue
|
|
||||||
if ($mysql) { return Split-Path $mysql.Source -Parent }
|
|
||||||
|
|
||||||
$xampp = Find-Xampp
|
|
||||||
if ($xampp -and (Test-Path "$xampp\mysql\bin\mysql.exe")) { return "$xampp\mysql\bin" }
|
|
||||||
|
|
||||||
$wamp = Find-Wamp
|
|
||||||
if ($wamp -and (Test-Path "$wamp\bin\mariadb\*\bin\mysql.exe")) {
|
|
||||||
$dirs = Get-ChildItem "$wamp\bin\mariadb" -Directory | Sort-Object Name -Descending
|
|
||||||
if ($dirs.Count -gt 0) { return "$($dirs[0].FullName)\bin" }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-Apache {
|
|
||||||
$xampp = Find-Xampp
|
|
||||||
if ($xampp -and (Test-Path "$xampp\apache\bin\httpd.exe")) { return "$xampp\apache" }
|
|
||||||
|
|
||||||
$wamp = Find-Wamp
|
|
||||||
if ($wamp -and (Test-Path "$wamp\bin\apache\*\bin\httpd.exe")) {
|
|
||||||
$dirs = Get-ChildItem "$wamp\bin\apache" -Directory | Sort-Object Name -Descending
|
|
||||||
if ($dirs.Count -gt 0) { return $dirs[0].FullName }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-Tool {
|
|
||||||
param([string]$Name, [string]$CheckCommand, [string]$Url, [string]$InstallHint)
|
|
||||||
|
|
||||||
$found = Get-Command $CheckCommand -ErrorAction SilentlyContinue
|
|
||||||
if ($found) {
|
|
||||||
Write-OK "$Name is installed"
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Warn "$Name is not found."
|
|
||||||
if ($Url) {
|
|
||||||
Write-Info "Download from: $Url"
|
|
||||||
}
|
|
||||||
if ($InstallHint) {
|
|
||||||
Write-Info "$InstallHint"
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Install-Composer {
|
|
||||||
if (Get-Command "composer" -ErrorAction SilentlyContinue) {
|
|
||||||
Write-OK "Composer already installed"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Write-Step "Installing Composer..."
|
|
||||||
$installer = "$env:TEMP\composer-setup.php"
|
|
||||||
Invoke-WebRequest -Uri "https://getcomposer.org/installer" -OutFile $installer
|
|
||||||
php $installer --quiet --install-dir="$env:USERPROFILE\bin"
|
|
||||||
php -r "unlink('$installer');"
|
|
||||||
$env:Path += ";$env:USERPROFILE\bin"
|
|
||||||
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:USERPROFILE\bin", "User")
|
|
||||||
Write-OK "Composer installed. You may need to restart your terminal."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Clone-Project {
|
|
||||||
param([string]$TargetDir)
|
|
||||||
|
|
||||||
if (Test-Path "$TargetDir\artisan") {
|
|
||||||
Write-OK "Project already cloned at $TargetDir"
|
|
||||||
return $TargetDir
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Error "Git is not installed. Install Git for Windows: https://git-scm.com/download/win"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Step "Cloning project..."
|
|
||||||
git clone $REPO_URL $TargetDir
|
|
||||||
Write-OK "Project cloned to $TargetDir"
|
|
||||||
return $TargetDir
|
|
||||||
}
|
|
||||||
|
|
||||||
function Configure-Env {
|
|
||||||
param([string]$ProjectDir)
|
|
||||||
|
|
||||||
Write-Step "Configuring environment..."
|
|
||||||
$envFile = "$ProjectDir\.env"
|
|
||||||
if (Test-Path $envFile) {
|
|
||||||
Write-Info ".env already exists, skipping"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Copy-Item "$ProjectDir\.env.example" $envFile
|
|
||||||
$dbPass = Read-Host "Database password (leave empty for root without password)"
|
|
||||||
|
|
||||||
(Get-Content $envFile) -replace 'DB_DATABASE=.*', "DB_DATABASE=$DB_NAME" | Set-Content $envFile
|
|
||||||
(Get-Content $envFile) -replace 'DB_USERNAME=.*', "DB_USERNAME=$DB_USER" | Set-Content $envFile
|
|
||||||
(Get-Content $envFile) -replace 'DB_PASSWORD=.*', "DB_PASSWORD=$dbPass" | Set-Content $envFile
|
|
||||||
|
|
||||||
Push-Location $ProjectDir
|
|
||||||
php artisan key:generate --quiet
|
|
||||||
Pop-Location
|
|
||||||
|
|
||||||
Write-OK ".env configured and APP_KEY generated"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Create-Database {
|
|
||||||
param([string]$MySQLDir)
|
|
||||||
|
|
||||||
Write-Step "Creating database '$DB_NAME'..."
|
|
||||||
|
|
||||||
$mysqlExe = if ($MySQLDir) { "$MySQLDir\mysql.exe" } else { "mysql.exe" }
|
|
||||||
|
|
||||||
if ($DB_PASS) {
|
|
||||||
& $mysqlExe -u $DB_USER -p$DB_PASS -e "CREATE DATABASE IF NOT EXISTS `$DB_NAME` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>$null
|
|
||||||
} else {
|
|
||||||
& $mysqlExe -u $DB_USER -e "CREATE DATABASE IF NOT EXISTS `$DB_NAME` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>$null
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
Write-OK "Database '$DB_NAME' ready"
|
|
||||||
} else {
|
|
||||||
Write-Warn "Could not create database automatically."
|
|
||||||
Write-Info "Open phpMyAdmin (http://localhost/phpmyadmin) and create a database named '$DB_NAME' with charset utf8mb4_unicode_ci."
|
|
||||||
Read-Host "Press Enter after creating the database"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Run-Migrations {
|
|
||||||
param([string]$ProjectDir)
|
|
||||||
|
|
||||||
Write-Step "Running migrations and seeders..."
|
|
||||||
Push-Location $ProjectDir
|
|
||||||
php artisan migrate --seed --force
|
|
||||||
Pop-Location
|
|
||||||
Write-OK "Migrations and seeders complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-Assets {
|
|
||||||
param([string]$ProjectDir)
|
|
||||||
|
|
||||||
Write-Step "Building frontend assets..."
|
|
||||||
Push-Location $ProjectDir
|
|
||||||
yarn build:all
|
|
||||||
Pop-Location
|
|
||||||
Write-OK "Frontend assets built"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Set-Permissions {
|
|
||||||
param([string]$ProjectDir)
|
|
||||||
|
|
||||||
Write-Step "Setting permissions..."
|
|
||||||
$folders = @("$ProjectDir\storage", "$ProjectDir\bootstrap\cache")
|
|
||||||
|
|
||||||
foreach ($folder in $folders) {
|
|
||||||
if (Test-Path $folder) {
|
|
||||||
try {
|
|
||||||
icacls $folder /grant "Everyone:F" /T /Q 2>$null
|
|
||||||
} catch {
|
|
||||||
Write-Warn "Could not set permissions on $folder. You may need to do this manually."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-OK "Permissions set"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Configure-Apache {
|
|
||||||
param([string]$ApacheDir, [string]$ProjectDir)
|
|
||||||
|
|
||||||
Write-Step "Configuring Apache virtual host..."
|
|
||||||
|
|
||||||
$domain = Read-Host "Enter your domain name (e.g. localhost)"
|
|
||||||
$publicDir = "$ProjectDir\public".Replace("\", "/")
|
|
||||||
|
|
||||||
$vhost = @"
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName $domain
|
|
||||||
DocumentRoot "$publicDir"
|
|
||||||
|
|
||||||
<Directory "$publicDir">
|
|
||||||
Options Indexes FollowSymLinks
|
|
||||||
AllowOverride All
|
|
||||||
Require all granted
|
|
||||||
</Directory>
|
|
||||||
|
|
||||||
ErrorLog logs/atomcms-error.log
|
|
||||||
CustomLog logs/atomcms-access.log combined
|
|
||||||
</VirtualHost>
|
|
||||||
"@
|
|
||||||
|
|
||||||
if ($ApacheDir) {
|
|
||||||
$confFile = "$ApacheDir\conf\extra\httpd-vhosts.conf"
|
|
||||||
Add-Content -Path $confFile -Value "`n$vhost" -ErrorAction SilentlyContinue
|
|
||||||
Write-OK "Virtual host added to $confFile"
|
|
||||||
Write-Info "Make sure these lines are uncommented in httpd.conf:"
|
|
||||||
Write-Info " Include conf/extra/httpd-vhosts.conf"
|
|
||||||
Write-Info " LoadModule rewrite_module modules/mod_rewrite.so"
|
|
||||||
} else {
|
|
||||||
Write-Warn "Apache directory not found. Add this virtual host manually:"
|
|
||||||
Write-Host $vhost -ForegroundColor Gray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Show-Summary {
|
|
||||||
param([string]$Domain)
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
|
||||||
Write-Host "║ AtomCMS is ready! ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ Visit: http://$Domain ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ Admin: http://$Domain/admin ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ Run the repair tool for extra checks: ║" -ForegroundColor Green
|
|
||||||
Write-Host "║ php artisan atom:check --fix ║" -ForegroundColor Green
|
|
||||||
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function Main {
|
|
||||||
Write-Logo
|
|
||||||
Test-Admin
|
|
||||||
|
|
||||||
# Check environment
|
|
||||||
$phpDir = Find-PHP
|
|
||||||
$mysqlDir = Find-MySQL
|
|
||||||
$apacheDir = Find-Apache
|
|
||||||
|
|
||||||
if (-not $phpDir) {
|
|
||||||
Write-Error "PHP not found. Install XAMPP (https://www.apachefriends.org/) or WampServer first."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add PHP and MySQL to PATH for this session
|
|
||||||
if ($phpDir) { $env:Path = "$phpDir;$env:Path" }
|
|
||||||
if ($mysqlDir) { $env:Path = "$mysqlDir;$env:Path" }
|
|
||||||
|
|
||||||
Write-OK "PHP found: $((Get-Command php).Source)"
|
|
||||||
if ($mysqlDir) { Write-OK "MySQL found: $mysqlDir" }
|
|
||||||
if ($apacheDir) { Write-OK "Apache found: $apacheDir" }
|
|
||||||
|
|
||||||
# Ensure required tools
|
|
||||||
Ensure-Tool -Name "Node.js" -CheckCommand "node" -Url "https://nodejs.org/" -InstallHint "Install Node.js LTS"
|
|
||||||
$hasYarn = Ensure-Tool -Name "Yarn" -CheckCommand "yarn" -Url "https://yarnpkg.com/getting-started/install" -InstallHint "Run: npm install -g yarn"
|
|
||||||
Ensure-Tool -Name "Git" -CheckCommand "git" -Url "https://git-scm.com/download/win" -InstallHint "Install Git for Windows"
|
|
||||||
|
|
||||||
if (-not $hasYarn) {
|
|
||||||
Write-Step "Installing Yarn via npm..."
|
|
||||||
npm install -g yarn
|
|
||||||
Write-OK "Yarn installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
Install-Composer
|
|
||||||
|
|
||||||
# Where to install?
|
|
||||||
$defaultDir = if (Find-Xampp) { "$(Find-Xampp)\htdocs\atomcms" } else { "$env:USERPROFILE\atomcms" }
|
|
||||||
$projectDir = Read-Host "Installation directory [$defaultDir]"
|
|
||||||
if (-not $projectDir) { $projectDir = $defaultDir }
|
|
||||||
|
|
||||||
Clone-Project -TargetDir $projectDir
|
|
||||||
Configure-Env -ProjectDir $projectDir
|
|
||||||
|
|
||||||
# Install PHP deps
|
|
||||||
Write-Step "Installing PHP dependencies..."
|
|
||||||
Push-Location $projectDir
|
|
||||||
composer install --no-interaction --optimize-autoloader --no-dev
|
|
||||||
Pop-Location
|
|
||||||
Write-OK "PHP dependencies installed"
|
|
||||||
|
|
||||||
# Install Node deps
|
|
||||||
Write-Step "Installing Node dependencies..."
|
|
||||||
Push-Location $projectDir
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
Pop-Location
|
|
||||||
Write-OK "Node dependencies installed"
|
|
||||||
|
|
||||||
Create-Database -MySQLDir $mysqlDir
|
|
||||||
Run-Migrations -ProjectDir $projectDir
|
|
||||||
Build-Assets -ProjectDir $projectDir
|
|
||||||
Set-Permissions -ProjectDir $projectDir
|
|
||||||
|
|
||||||
$configureWeb = Read-Host "Configure Apache virtual host? (y/N)"
|
|
||||||
if ($configureWeb -eq "y") {
|
|
||||||
Configure-Apache -ApacheDir $apacheDir -ProjectDir $projectDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Finalize
|
|
||||||
Write-Step "Finalizing..."
|
|
||||||
Push-Location $projectDir
|
|
||||||
php artisan storage:link --force 2>$null
|
|
||||||
php artisan optimize:clear 2>$null
|
|
||||||
Pop-Location
|
|
||||||
|
|
||||||
$domain = Read-Host "Enter your domain/path to access the site (e.g. localhost/atomcms/public or your-domain.com)"
|
|
||||||
Show-Summary -Domain $domain
|
|
||||||
}
|
|
||||||
|
|
||||||
Main
|
|
||||||
Executable → Regular
+1509
-1513
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1509
-1513
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Executable → Regular
+305
-349
@@ -1,351 +1,307 @@
|
|||||||
{
|
{
|
||||||
"commandocentrum.live_status": "Live Status",
|
"commandocentrum.live_status": "Live Status",
|
||||||
"commandocentrum.live_status_desc": "Real-time hotel statistics",
|
"commandocentrum.live_status_desc": "Real-time hotel statistics",
|
||||||
"commandocentrum.online": "Online",
|
"commandocentrum.online": "Online",
|
||||||
"commandocentrum.emulator": "Emulator",
|
"commandocentrum.emulator": "Emulator",
|
||||||
"commandocentrum.database": "Database",
|
"commandocentrum.database": "Database",
|
||||||
"commandocentrum.load": "Load",
|
"commandocentrum.load": "Load",
|
||||||
"commandocentrum.server_info": "Server Information",
|
"commandocentrum.server_info": "Server Information",
|
||||||
"commandocentrum.server_info_desc": "Detailed server status",
|
"commandocentrum.server_info_desc": "Detailed server status",
|
||||||
"commandocentrum.php_laravel": "PHP & Laravel",
|
"commandocentrum.php_laravel": "PHP & Laravel",
|
||||||
"commandocentrum.memory_disk": "Memory & Disk",
|
"commandocentrum.memory_disk": "Memory & Disk",
|
||||||
"commandocentrum.memory": "Memory",
|
"commandocentrum.memory": "Memory",
|
||||||
"commandocentrum.disk": "Disk",
|
"commandocentrum.disk": "Disk",
|
||||||
"commandocentrum.uptime": "Uptime",
|
"commandocentrum.uptime": "Uptime",
|
||||||
"commandocentrum.system_health": "System Health",
|
"commandocentrum.system_health": "System Health",
|
||||||
"commandocentrum.system_health_desc": "Automatic system diagnostics",
|
"commandocentrum.system_health_desc": "Automatic system diagnostics",
|
||||||
"commandocentrum.refresh": "Refresh",
|
"commandocentrum.refresh": "Refresh",
|
||||||
"commandocentrum.healthy": "Healthy",
|
"commandocentrum.healthy": "Healthy",
|
||||||
"commandocentrum.warnings": "Warnings",
|
"commandocentrum.warnings": "Warnings",
|
||||||
"commandocentrum.errors": "Errors",
|
"commandocentrum.errors": "Errors",
|
||||||
"commandocentrum.system_status": "System Status",
|
"commandocentrum.system_status": "System Status",
|
||||||
"commandocentrum.critical_issues": "Critical Issues",
|
"commandocentrum.critical_issues": "Critical Issues",
|
||||||
"commandocentrum.hotel_status": "Hotel Status",
|
"commandocentrum.hotel_status": "Hotel Status",
|
||||||
"commandocentrum.hotel_status_desc": "Emulator and Nitro status",
|
"commandocentrum.hotel_status_desc": "Emulator and Nitro status",
|
||||||
"commandocentrum.hotel_alert": "Hotel Alert",
|
"commandocentrum.hotel_alert": "Hotel Alert",
|
||||||
"commandocentrum.hotel_alert_desc": "Send a message to all online users",
|
"commandocentrum.hotel_alert_desc": "Send a message to all online users",
|
||||||
"commandocentrum.send_alert": "Send Alert",
|
"commandocentrum.send_alert": "Send Alert",
|
||||||
"commandocentrum.alert_message_placeholder": "Type your alert message here...",
|
"commandocentrum.alert_message_placeholder": "Type your alert message here...",
|
||||||
"commandocentrum.emulator_logs": "Emulator Logs",
|
"commandocentrum.emulator_logs": "Emulator Logs",
|
||||||
"commandocentrum.emulator_logs_desc": "Live emulator log viewer",
|
"commandocentrum.emulator_logs_desc": "Live emulator log viewer",
|
||||||
"commandocentrum.emulator_control": "Emulator Control",
|
"commandocentrum.emulator_control": "Emulator Control",
|
||||||
"commandocentrum.emulator_control_desc": "Full emulator control",
|
"commandocentrum.emulator_control_desc": "Full emulator control",
|
||||||
"commandocentrum.start": "Start",
|
"commandocentrum.start": "Start",
|
||||||
"commandocentrum.stop": "Stop",
|
"commandocentrum.stop": "Stop",
|
||||||
"commandocentrum.restart": "Restart",
|
"commandocentrum.restart": "Restart",
|
||||||
"commandocentrum.check": "Check",
|
"commandocentrum.check": "Check",
|
||||||
"commandocentrum.version": "Version",
|
"commandocentrum.version": "Version",
|
||||||
"commandocentrum.service": "Service",
|
"commandocentrum.service": "Service",
|
||||||
"commandocentrum.status": "Status",
|
"commandocentrum.status": "Status",
|
||||||
"commandocentrum.emulator_updates": "Emulator Updates",
|
"commandocentrum.emulator_updates_desc": "Configure and update the emulator",
|
||||||
"commandocentrum.emulator_updates_desc": "Configure and update the emulator",
|
"commandocentrum.build": "Build",
|
||||||
"commandocentrum.check_updates": "Check Updates",
|
"commandocentrum.save": "Save",
|
||||||
"commandocentrum.build": "Build",
|
"commandocentrum.github_url": "GitHub URL",
|
||||||
"commandocentrum.sql_updates": "SQL Updates",
|
"commandocentrum.jar_direct_url": "JAR Direct URL",
|
||||||
"commandocentrum.save": "Save",
|
"commandocentrum.jar_path": "JAR Path",
|
||||||
"commandocentrum.github_url": "GitHub URL",
|
"commandocentrum.source_repo": "Source Repo",
|
||||||
"commandocentrum.jar_direct_url": "JAR Direct URL",
|
"commandocentrum.source_path": "Source Path",
|
||||||
"commandocentrum.jar_path": "JAR Path",
|
"commandocentrum.branch": "Branch",
|
||||||
"commandocentrum.source_repo": "Source Repo",
|
"commandocentrum.db_host": "DB Host",
|
||||||
"commandocentrum.source_path": "Source Path",
|
"commandocentrum.db_name": "DB Name",
|
||||||
"commandocentrum.branch": "Branch",
|
"commandocentrum.service_name": "Service Name",
|
||||||
"commandocentrum.db_host": "DB Host",
|
"commandocentrum.emulator_backups_desc": "View and restore emulator backups",
|
||||||
"commandocentrum.db_name": "DB Name",
|
"commandocentrum.restore": "Restore",
|
||||||
"commandocentrum.service_name": "Service Name",
|
"commandocentrum.nitro_client": "Nitro Client",
|
||||||
"commandocentrum.emulator_backups": "Emulator Backups",
|
"commandocentrum.nitro_cli_only": "This script can only be run via command line. Use: bash update-Nitrov3.sh\n\nSettings are configured in the .env file in the project root.",
|
||||||
"commandocentrum.emulator_backups_desc": "View and restore emulator backups",
|
"commandocentrum.clothing_sync": "Clothing Sync",
|
||||||
"commandocentrum.no_backups": "No backups available yet",
|
"commandocentrum.clothing_sync_desc": "Sync catalog clothing from FigureMap",
|
||||||
"commandocentrum.backups_auto": "Backups are automatically created on every emulator update",
|
"commandocentrum.sync": "Sync",
|
||||||
"commandocentrum.restore": "Restore",
|
"commandocentrum.clothing_items": "Clothing Items",
|
||||||
"commandocentrum.nitro_client": "Nitro Client",
|
"commandocentrum.notifications": "Notifications",
|
||||||
"commandocentrum.nitro_client_desc": "Configure and update Nitro",
|
"commandocentrum.notifications_desc": "Email and Discord alerts",
|
||||||
"commandocentrum.auto_detect": "Auto Detect",
|
"commandocentrum.test_discord": "Test Discord",
|
||||||
"commandocentrum.generate_configs": "Generate Configs",
|
"commandocentrum.email_notifications": "Email Notifications",
|
||||||
"commandocentrum.client_path": "Client Path",
|
"commandocentrum.email_address": "Email Address",
|
||||||
"commandocentrum.renderer_path": "Renderer Path",
|
"commandocentrum.discord_notifications": "Discord Notifications",
|
||||||
"commandocentrum.build_path": "Build Path",
|
"commandocentrum.webhook_url": "Webhook URL",
|
||||||
"commandocentrum.webroot": "Webroot",
|
"commandocentrum.discord_ranks": "Ranks that receive Discord notifications",
|
||||||
"commandocentrum.site_url": "Site URL",
|
"commandocentrum.discord_ranks_helper": "Leave empty for staff only (min_staff_rank)",
|
||||||
"commandocentrum.auto_updates": "Automatic Updates",
|
"commandocentrum.social_login": "Social Login (v1.4)",
|
||||||
"commandocentrum.auto_updates_desc": "Configure automatic updates",
|
"commandocentrum.social_login_desc": "Enable social login providers",
|
||||||
"commandocentrum.enable_auto_updates": "Enable Automatic Updates",
|
"commandocentrum.google_login": "Google Login",
|
||||||
"commandocentrum.schedule": "Schedule (HH:MM)",
|
"commandocentrum.google_login_helper": "Allow users to login with Google",
|
||||||
"commandocentrum.days": "Days (0-6)",
|
"commandocentrum.google_client_id": "Google Client ID",
|
||||||
"commandocentrum.clothing_sync": "Clothing Sync",
|
"commandocentrum.google_client_id_helper": "From Google Cloud Console",
|
||||||
"commandocentrum.clothing_sync_desc": "Sync catalog clothing from FigureMap",
|
"commandocentrum.google_client_secret": "Google Client Secret",
|
||||||
"commandocentrum.sync": "Sync",
|
"commandocentrum.discord_login": "Discord Login",
|
||||||
"commandocentrum.clothing_items": "Clothing Items",
|
"commandocentrum.discord_login_helper": "Allow users to login with Discord",
|
||||||
"commandocentrum.notifications": "Notifications",
|
"commandocentrum.discord_client_id": "Discord Client ID",
|
||||||
"commandocentrum.notifications_desc": "Email and Discord alerts",
|
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
|
||||||
"commandocentrum.test_discord": "Test Discord",
|
"commandocentrum.discord_client_secret": "Discord Client Secret",
|
||||||
"commandocentrum.email_notifications": "Email Notifications",
|
"commandocentrum.github_login": "GitHub Login",
|
||||||
"commandocentrum.email_address": "Email Address",
|
"commandocentrum.github_login_helper": "Allow users to login with GitHub",
|
||||||
"commandocentrum.discord_notifications": "Discord Notifications",
|
"commandocentrum.github_client_id": "GitHub Client ID",
|
||||||
"commandocentrum.webhook_url": "Webhook URL",
|
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
|
||||||
"commandocentrum.discord_ranks": "Ranks that receive Discord notifications",
|
"commandocentrum.github_client_secret": "GitHub Client Secret",
|
||||||
"commandocentrum.discord_ranks_helper": "Leave empty for staff only (min_staff_rank)",
|
"commandocentrum.staff_activity": "Staff Activity Log",
|
||||||
"commandocentrum.update_history": "Update History",
|
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
|
||||||
"commandocentrum.update_history_desc": "Latest system updates",
|
"commandocentrum.recent_staff_activities": "Recent Staff Activities",
|
||||||
"commandocentrum.no_updates_found": "No updates found",
|
"commandocentrum.last_20_actions": "Last 20 actions",
|
||||||
"commandocentrum.social_login": "Social Login (v1.4)",
|
"commandocentrum.no_staff_activities": "No staff activities recorded yet.",
|
||||||
"commandocentrum.social_login_desc": "Enable social login providers",
|
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
|
||||||
"commandocentrum.google_login": "Google Login",
|
"commandocentrum.error_loading_activities": "Error loading staff activities",
|
||||||
"commandocentrum.google_login_helper": "Allow users to login with Google",
|
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
|
||||||
"commandocentrum.google_client_id": "Google Client ID",
|
"commandocentrum.just_now": "Just now",
|
||||||
"commandocentrum.google_client_id_helper": "From Google Cloud Console",
|
"commandocentrum.minutes_ago": "m ago",
|
||||||
"commandocentrum.google_client_secret": "Google Client Secret",
|
"commandocentrum.hours_ago": "h ago",
|
||||||
"commandocentrum.discord_login": "Discord Login",
|
"commandocentrum.days_ago": "d ago",
|
||||||
"commandocentrum.discord_login_helper": "Allow users to login with Discord",
|
"commandocentrum.success": "Success",
|
||||||
"commandocentrum.discord_client_id": "Discord Client ID",
|
"commandocentrum.error": "Error",
|
||||||
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
|
"commandocentrum.warning": "Warning",
|
||||||
"commandocentrum.discord_client_secret": "Discord Client Secret",
|
"commandocentrum.info": "Info",
|
||||||
"commandocentrum.github_login": "GitHub Login",
|
"commandocentrum.emulator_started": "Emulator started!",
|
||||||
"commandocentrum.github_login_helper": "Allow users to login with GitHub",
|
"commandocentrum.emulator_start_failed": "Could not start emulator",
|
||||||
"commandocentrum.github_client_id": "GitHub Client ID",
|
"commandocentrum.emulator_stopped": "Emulator stopped!",
|
||||||
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
|
"commandocentrum.emulator_stop_failed": "Could not stop emulator",
|
||||||
"commandocentrum.github_client_secret": "GitHub Client Secret",
|
"commandocentrum.emulator_restarted": "Emulator restarted!",
|
||||||
"commandocentrum.staff_activity": "Staff Activity Log",
|
"commandocentrum.emulator_restart_failed": "Could not restart emulator",
|
||||||
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
|
"commandocentrum.emulator_online": "Emulator is online and responding!",
|
||||||
"commandocentrum.recent_staff_activities": "Recent Staff Activities",
|
"commandocentrum.emulator_unreachable": "Emulator is not reachable via RCON",
|
||||||
"commandocentrum.last_20_actions": "Last 20 actions",
|
"commandocentrum.emulator_settings_saved": "Emulator settings saved!",
|
||||||
"commandocentrum.no_staff_activities": "No staff activities recorded yet.",
|
"commandocentrum.alerts_saved": "Notifications saved!",
|
||||||
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
|
"commandocentrum.test_sent": "Test message sent!",
|
||||||
"commandocentrum.error_loading_activities": "Error loading staff activities",
|
"commandocentrum.webhook_empty": "Webhook URL is empty",
|
||||||
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
|
"commandocentrum.diagnostics_refreshed": "Diagnostics refreshed",
|
||||||
"commandocentrum.just_now": "Just now",
|
"commandocentrum.unknown": "Unknown",
|
||||||
"commandocentrum.minutes_ago": "m ago",
|
"commandocentrum.not_applicable": "N/A",
|
||||||
"commandocentrum.hours_ago": "h ago",
|
"commandocentrum.offline": "Offline",
|
||||||
"commandocentrum.days_ago": "d ago",
|
"commandocentrum.active": "Active",
|
||||||
"commandocentrum.success": "Success",
|
"commandocentrum.inactive": "Inactive",
|
||||||
"commandocentrum.error": "Error",
|
"commandocentrum.not_found": "Not found",
|
||||||
"commandocentrum.warning": "Warning",
|
"commandocentrum.ok": "OK",
|
||||||
"commandocentrum.info": "Info",
|
"commandocentrum.missing": "Missing",
|
||||||
"commandocentrum.emulator_started": "Emulator started!",
|
"commandocentrum.jars": "JARs",
|
||||||
"commandocentrum.emulator_start_failed": "Could not start emulator",
|
"commandocentrum.source": "Source",
|
||||||
"commandocentrum.emulator_stopped": "Emulator stopped!",
|
"commandocentrum.method": "Method",
|
||||||
"commandocentrum.emulator_stop_failed": "Could not stop emulator",
|
"commandocentrum.jar_download_restart": "JAR Download & Restart",
|
||||||
"commandocentrum.emulator_restarted": "Emulator restarted!",
|
"commandocentrum.maven_build_restart": "Maven Build & Restart",
|
||||||
"commandocentrum.emulator_restart_failed": "Could not restart emulator",
|
"commandocentrum.manual_download": "Manual: Download JAR from GitHub",
|
||||||
"commandocentrum.emulator_online": "Emulator is online and responding!",
|
"commandocentrum.maven_pom": "Maven (pom.xml)",
|
||||||
"commandocentrum.emulator_unreachable": "Emulator is not reachable via RCON",
|
"commandocentrum.no_pom": "No pom.xml",
|
||||||
"commandocentrum.building_emulator": "Emulator is being built from source...",
|
"commandocentrum.update_available": "Update available",
|
||||||
"commandocentrum.emulator_built": "Emulator built!",
|
"commandocentrum.up_to_date": "Up-to-date",
|
||||||
"commandocentrum.build_failed": "Build failed",
|
"commandocentrum.update": "Update",
|
||||||
"commandocentrum.configure_github_url": "Please configure the Emulator GitHub URL first",
|
"commandocentrum.rebuild": "Rebuild",
|
||||||
"commandocentrum.maven_not_installed": "Maven (mvn) is not installed - cannot build",
|
"commandocentrum.latest": "Latest",
|
||||||
"commandocentrum.building_maven": "Building emulator with Maven...",
|
"commandocentrum.remote": "Remote",
|
||||||
"commandocentrum.build_success_jar": "Build successful! JAR moved to :jar. Restart the emulator.",
|
"commandocentrum.local": "Local",
|
||||||
"commandocentrum.build_success": "Build successful! Restart the emulator.",
|
"commandocentrum.client": "Client",
|
||||||
"commandocentrum.build_failed_logs": "Build failed - check logs",
|
"commandocentrum.renderer": "Renderer",
|
||||||
"commandocentrum.no_pom_xml": "No pom.xml found - cannot build from source",
|
"commandocentrum.webroot_status": "Webroot",
|
||||||
"commandocentrum.sql_applied": "SQL updates applied!",
|
"commandocentrum.rank": "Rank",
|
||||||
"commandocentrum.update_complete": "Update check completed",
|
"radio.title": "Radio",
|
||||||
"commandocentrum.emulator_settings_saved": "Emulator settings saved!",
|
"radio.music": "Music",
|
||||||
"commandocentrum.nitro_updated": "Nitro updated! Build again with \"Build\" button.",
|
"radio.loading": "Loading...",
|
||||||
"commandocentrum.nitro_up_to_date": "Nitro is already up-to-date!",
|
"radio.navigation_label": "Radio",
|
||||||
"commandocentrum.building_nitro": "Building Nitro...",
|
"radio.setup_page_title": "Radio Setup",
|
||||||
"commandocentrum.nitro_build_success": "Nitro build successful!",
|
"radio.setup_page_subtitle": "Configure your radio system in one go",
|
||||||
"commandocentrum.nitro_build_warning": "Build started - check manually",
|
"radio.setup.success_title": "Radio Installed!",
|
||||||
"commandocentrum.valid_url_required": "Please enter a valid URL (e.g. https://epicnabbo.nl)",
|
"radio.setup.success_body": "Radio system has been successfully installed and configured!",
|
||||||
"commandocentrum.configs_generated": "Configs generated & existing settings preserved!",
|
"radio.setup.error_title": "Installation Failed",
|
||||||
"commandocentrum.config_generated_warning": "Config generated (check manually)",
|
"radio.setup.error_body": "An error occurred: :message",
|
||||||
"commandocentrum.paths_detected": "Paths detected and saved!",
|
"radio.setup.button_label": "Install Everything",
|
||||||
"commandocentrum.nitro_settings_saved": "Nitro settings saved!",
|
"radio.setup.modal_heading": "Install Radio?",
|
||||||
"commandocentrum.auto_update_saved": "Auto update settings saved!",
|
"radio.setup.modal_description": "This will configure all radio settings with default values.",
|
||||||
"commandocentrum.alerts_saved": "Notifications saved!",
|
"radio.setup.modal_submit": "Yes, install!",
|
||||||
"commandocentrum.test_sent": "Test message sent!",
|
"radio.setup.tooltip": "Install the complete radio system",
|
||||||
"commandocentrum.webhook_empty": "Webhook URL is empty",
|
"radio.setup_complete": "✅ Installation Complete!",
|
||||||
"commandocentrum.diagnostics_refreshed": "Diagnostics refreshed",
|
"radio.what_gets_configured": "What gets configured?",
|
||||||
"commandocentrum.unknown": "Unknown",
|
"radio.radio_stream": "Radio Stream",
|
||||||
"commandocentrum.not_applicable": "N/A",
|
"radio.radio_stream_desc": "Set your stream URL with support for SHOUTcast, Icecast, AzureCast and other streaming platforms.",
|
||||||
"commandocentrum.offline": "Offline",
|
"radio.points_system": "Points System",
|
||||||
"commandocentrum.active": "Active",
|
"radio.points_system_desc": "Let users earn points by listening, requesting songs and participating in contests.",
|
||||||
"commandocentrum.inactive": "Inactive",
|
"radio.community_features": "Community Features",
|
||||||
"commandocentrum.not_found": "Not found",
|
"radio.community_features_desc": "Shouts, song requests, DJ applications and more community interactions.",
|
||||||
"commandocentrum.ok": "OK",
|
"radio.dj_management": "DJ Management",
|
||||||
"commandocentrum.missing": "Missing",
|
"radio.dj_management_desc": "DJ ranks, schedule, auto-detection and Sambroadcaster/Virtual DJ integration.",
|
||||||
"commandocentrum.jars": "JARs",
|
"radio.monitoring": "Stream Monitoring",
|
||||||
"commandocentrum.source": "Source",
|
"radio.monitoring_desc": "Monitor your stream uptime with real-time monitoring.",
|
||||||
"commandocentrum.method": "Method",
|
"radio.display_options": "Display Options",
|
||||||
"commandocentrum.jar_download_restart": "JAR Download & Restart",
|
"radio.display_options_desc": "Widget, player styles, colors and custom CSS/JS.",
|
||||||
"commandocentrum.maven_build_restart": "Maven Build & Restart",
|
"radio.default_settings": "Default Settings",
|
||||||
"commandocentrum.manual_download": "Manual: Download JAR from GitHub",
|
"radio.radio_label": "Radio",
|
||||||
"commandocentrum.maven_pom": "Maven (pom.xml)",
|
"radio.enabled": "Enabled",
|
||||||
"commandocentrum.no_pom": "No pom.xml",
|
"radio.points_label": "Points",
|
||||||
"commandocentrum.update_available": "Update available",
|
"radio.per_min": " per min",
|
||||||
"commandocentrum.up_to_date": "Up-to-date",
|
"radio.daily_limit": "Daily limit",
|
||||||
"commandocentrum.update": "Update",
|
"radio.shouts_label": "Shouts",
|
||||||
"commandocentrum.rebuild": "Rebuild",
|
"radio.on": "On",
|
||||||
"commandocentrum.latest": "Latest",
|
"radio.widget": "Widget",
|
||||||
"commandocentrum.remote": "Remote",
|
"radio.global": "Global",
|
||||||
"commandocentrum.local": "Local",
|
"radio.dj_apps": "DJ Applications",
|
||||||
"commandocentrum.client": "Client",
|
"radio.open": "Open",
|
||||||
"commandocentrum.renderer": "Renderer",
|
"radio.monitoring_label": "Monitoring",
|
||||||
"commandocentrum.webroot_status": "Webroot",
|
"radio.contests_label": "Contests",
|
||||||
"commandocentrum.rank": "Rank",
|
"radio.install_radio_system": "🚀 Install Radio System",
|
||||||
"radio.title": "Radio",
|
"radio.reset_settings": "Reset Settings",
|
||||||
"radio.music": "Music",
|
"radio.reset_confirm": "Are you sure you want to reset all radio settings?",
|
||||||
"radio.loading": "Loading...",
|
"radio.go_to_radio_settings": "Go to Radio Settings",
|
||||||
"radio.navigation_label": "Radio",
|
"radio.open_wizard": "🎯 Open Radio Wizard",
|
||||||
"radio.setup_page_title": "Radio Setup",
|
"radio.wizard_desc": "Step-by-step wizard with connection test",
|
||||||
"radio.setup_page_subtitle": "Configure your radio system in one go",
|
"radio.wizard.title": "Radio Installation Wizard",
|
||||||
"radio.setup.success_title": "Radio Installed!",
|
"radio.wizard.step_short": "Step",
|
||||||
"radio.setup.success_body": "Radio system has been successfully installed and configured!",
|
"radio.wizard.step_prefix": "Step",
|
||||||
"radio.setup.error_title": "Installation Failed",
|
"radio.wizard.of": "of",
|
||||||
"radio.setup.error_body": "An error occurred: :message",
|
"radio.wizard.next_step": "Next Step →",
|
||||||
"radio.setup.button_label": "Install Everything",
|
"radio.wizard.previous_step": "← Previous Step",
|
||||||
"radio.setup.modal_heading": "Install Radio?",
|
"radio.wizard.back_to_setup": "Back to setup",
|
||||||
"radio.setup.modal_description": "This will configure all radio settings with default values.",
|
"radio.wizard.step1_label": "Platform",
|
||||||
"radio.setup.modal_submit": "Yes, install!",
|
"radio.wizard.step2_label": "Stream",
|
||||||
"radio.setup.tooltip": "Install the complete radio system",
|
"radio.wizard.step3_label": "API",
|
||||||
"radio.setup_complete": "✅ Installation Complete!",
|
"radio.wizard.step4_label": "Features",
|
||||||
"radio.what_gets_configured": "What gets configured?",
|
"radio.wizard.step5_label": "Test",
|
||||||
"radio.radio_stream": "Radio Stream",
|
"radio.wizard.step1_subtitle": "Choose your streaming platform",
|
||||||
"radio.radio_stream_desc": "Set your stream URL with support for SHOUTcast, Icecast, AzureCast and other streaming platforms.",
|
"radio.wizard.step2_title": "Stream Configuration",
|
||||||
"radio.points_system": "Points System",
|
"radio.wizard.step3_title": "API Configuration",
|
||||||
"radio.points_system_desc": "Let users earn points by listening, requesting songs and participating in contests.",
|
"radio.wizard.step3_subtitle": "Now Playing & Listeners",
|
||||||
"radio.community_features": "Community Features",
|
"radio.wizard.step4_title": "Configure Features",
|
||||||
"radio.community_features_desc": "Shouts, song requests, DJ applications and more community interactions.",
|
"radio.wizard.step4_subtitle": "Choose which radio features to enable",
|
||||||
"radio.dj_management": "DJ Management",
|
"radio.wizard.step5_title": "Test & Install",
|
||||||
"radio.dj_management_desc": "DJ ranks, schedule, auto-detection and Sambroadcaster/Virtual DJ integration.",
|
"radio.wizard.step5_subtitle": "Check the connection and complete the installation",
|
||||||
"radio.monitoring": "Stream Monitoring",
|
"radio.wizard.platform_shoutcast": "SHOUTcast",
|
||||||
"radio.monitoring_desc": "Monitor your stream uptime with real-time monitoring.",
|
"radio.wizard.platform_shoutcast_desc": "For SHOUTcast servers. Auto-detection of now playing and listeners via stats endpoint.",
|
||||||
"radio.display_options": "Display Options",
|
"radio.wizard.platform_icecast": "Icecast",
|
||||||
"radio.display_options_desc": "Widget, player styles, colors and custom CSS/JS.",
|
"radio.wizard.platform_icecast_desc": "For Icecast servers. Uses status-json.xsl for auto-detection.",
|
||||||
"radio.default_settings": "Default Settings",
|
"radio.wizard.platform_azurecast": "AzureCast",
|
||||||
"radio.radio_label": "Radio",
|
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Full API integration with now-playing, listeners and auto-configuration.",
|
||||||
"radio.enabled": "Enabled",
|
"radio.wizard.platform_other": "Other",
|
||||||
"radio.points_label": "Points",
|
"radio.wizard.platform_other_desc": "Another stream provider. Manual configuration of stream URL and API endpoints.",
|
||||||
"radio.per_min": " per min",
|
"radio.wizard.shoutcast_info_title": "SHOUTcast",
|
||||||
"radio.daily_limit": "Daily limit",
|
"radio.wizard.shoutcast_info_desc": "Enter your SHOUTcast stream URL. The wizard will try to find the stats endpoint automatically.",
|
||||||
"radio.shouts_label": "Shouts",
|
"radio.wizard.icecast_info_title": "Icecast",
|
||||||
"radio.on": "On",
|
"radio.wizard.icecast_info_desc": "Enter your Icecast stream URL. The wizard uses status-json.xsl for auto-detection.",
|
||||||
"radio.widget": "Widget",
|
"radio.wizard.azurecast_info_title": "AzureCast",
|
||||||
"radio.global": "Global",
|
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuration. The wizard configures everything via the AzureCast API.",
|
||||||
"radio.dj_apps": "DJ Applications",
|
"radio.wizard.other_info_title": "Other Stream",
|
||||||
"radio.open": "Open",
|
"radio.wizard.other_info_desc": "Enter your stream URL. You can manually configure API endpoints for now playing and listeners later.",
|
||||||
"radio.monitoring_label": "Monitoring",
|
"radio.wizard.stream_url_label": "Stream URL *",
|
||||||
"radio.contests_label": "Contests",
|
"radio.wizard.stream_url_hint": "The direct URL to your audio stream (MP3, AAC, OGG, etc.)",
|
||||||
"radio.install_radio_system": "🚀 Install Radio System",
|
"radio.wizard.stream_name_label": "Stream Name",
|
||||||
"radio.reset_settings": "Reset Settings",
|
"radio.wizard.stream_name_placeholder": "My Radio",
|
||||||
"radio.reset_confirm": "Are you sure you want to reset all radio settings?",
|
"radio.wizard.stream_name_hint": "A name for your radio stream (optional)",
|
||||||
"radio.go_to_radio_settings": "Go to Radio Settings",
|
"radio.wizard.azurecast_section": "AzureCast Server Configuration",
|
||||||
"radio.open_wizard": "🎯 Open Radio Wizard",
|
"radio.wizard.azurecast_base_url_label": "AzureCast Base URL",
|
||||||
"radio.wizard_desc": "Step-by-step wizard with connection test",
|
"radio.wizard.azurecast_base_url_hint": "The base URL of your AzureCast server. Auto-detected if left empty.",
|
||||||
"radio.wizard.title": "Radio Installation Wizard",
|
"radio.wizard.azurecast_station_id_label": "Station ID",
|
||||||
"radio.wizard.step_short": "Step",
|
"radio.wizard.azurecast_station_id_hint": "The station ID in AzureCast (default: 1)",
|
||||||
"radio.wizard.step_prefix": "Step",
|
"radio.wizard.enable_now_playing": "Enable Now Playing",
|
||||||
"radio.wizard.of": "of",
|
"radio.wizard.now_playing_api_label": "Now Playing API URL",
|
||||||
"radio.wizard.next_step": "Next Step →",
|
"radio.wizard.now_playing_api_hint": "API endpoint that returns the current song. Usually auto-detected.",
|
||||||
"radio.wizard.previous_step": "← Previous Step",
|
"radio.wizard.enable_listeners": "Enable Listeners Counter",
|
||||||
"radio.wizard.back_to_setup": "Back to setup",
|
"radio.wizard.listeners_api_label": "Listeners API URL",
|
||||||
"radio.wizard.step1_label": "Platform",
|
"radio.wizard.listeners_api_hint": "API endpoint that returns the listener count.",
|
||||||
"radio.wizard.step2_label": "Stream",
|
"radio.wizard.enable_current_dj": "Show Current DJ",
|
||||||
"radio.wizard.step3_label": "API",
|
"radio.wizard.detected": "detected!",
|
||||||
"radio.wizard.step4_label": "Features",
|
"radio.wizard.detected_desc": "API endpoints were automatically found and filled in.",
|
||||||
"radio.wizard.step5_label": "Test",
|
"radio.wizard.not_detected": "No automatic detection",
|
||||||
"radio.wizard.step1_subtitle": "Choose your streaming platform",
|
"radio.wizard.not_detected_desc": "Fill in the API URLs manually or skip this step.",
|
||||||
"radio.wizard.step2_title": "Stream Configuration",
|
"radio.wizard.section_community": "Community Features",
|
||||||
"radio.wizard.step3_title": "API Configuration",
|
"radio.wizard.feature_shouts": "Shouts",
|
||||||
"radio.wizard.step3_subtitle": "Now Playing & Listeners",
|
"radio.wizard.feature_shouts_desc": "Leave messages",
|
||||||
"radio.wizard.step4_title": "Configure Features",
|
"radio.wizard.feature_applications": "DJ Applications",
|
||||||
"radio.wizard.step4_subtitle": "Choose which radio features to enable",
|
"radio.wizard.feature_applications_desc": "Apply as DJ",
|
||||||
"radio.wizard.step5_title": "Test & Install",
|
"radio.wizard.feature_requests": "Song Requests",
|
||||||
"radio.wizard.step5_subtitle": "Check the connection and complete the installation",
|
"radio.wizard.feature_requests_desc": "Request songs",
|
||||||
"radio.wizard.platform_shoutcast": "SHOUTcast",
|
"radio.wizard.section_display": "Display",
|
||||||
"radio.wizard.platform_shoutcast_desc": "For SHOUTcast servers. Auto-detection of now playing and listeners via stats endpoint.",
|
"radio.wizard.feature_widget": "Radio Widget",
|
||||||
"radio.wizard.platform_icecast": "Icecast",
|
"radio.wizard.feature_widget_desc": "Mini player on the site",
|
||||||
"radio.wizard.platform_icecast_desc": "For Icecast servers. Uses status-json.xsl for auto-detection.",
|
"radio.wizard.feature_widget_global": "Widget Everywhere",
|
||||||
"radio.wizard.platform_azurecast": "AzureCast",
|
"radio.wizard.feature_widget_global_desc": "Show on all pages",
|
||||||
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Full API integration with now-playing, listeners and auto-configuration.",
|
"radio.wizard.widget_position_label": "Widget Position",
|
||||||
"radio.wizard.platform_other": "Other",
|
"radio.wizard.position_bottom_right": "Bottom Right",
|
||||||
"radio.wizard.platform_other_desc": "Another stream provider. Manual configuration of stream URL and API endpoints.",
|
"radio.wizard.position_bottom_left": "Bottom Left",
|
||||||
"radio.wizard.shoutcast_info_title": "SHOUTcast",
|
"radio.wizard.position_top_right": "Top Right",
|
||||||
"radio.wizard.shoutcast_info_desc": "Enter your SHOUTcast stream URL. The wizard will try to find the stats endpoint automatically.",
|
"radio.wizard.position_top_left": "Top Left",
|
||||||
"radio.wizard.icecast_info_title": "Icecast",
|
"radio.wizard.section_gamification": "Gamification",
|
||||||
"radio.wizard.icecast_info_desc": "Enter your Icecast stream URL. The wizard uses status-json.xsl for auto-detection.",
|
"radio.wizard.feature_points": "Points System",
|
||||||
"radio.wizard.azurecast_info_title": "AzureCast",
|
"radio.wizard.feature_points_desc": "Earn points by listening",
|
||||||
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuration. The wizard configures everything via the AzureCast API.",
|
"radio.wizard.feature_contests": "Contests",
|
||||||
"radio.wizard.other_info_title": "Other Stream",
|
"radio.wizard.feature_contests_desc": "Organize competitions",
|
||||||
"radio.wizard.other_info_desc": "Enter your stream URL. You can manually configure API endpoints for now playing and listeners later.",
|
"radio.wizard.feature_giveaways": "Giveaways",
|
||||||
"radio.wizard.stream_url_label": "Stream URL *",
|
"radio.wizard.feature_giveaways_desc": "Give away prizes",
|
||||||
"radio.wizard.stream_url_hint": "The direct URL to your audio stream (MP3, AAC, OGG, etc.)",
|
"radio.wizard.section_integrations": "Integrations",
|
||||||
"radio.wizard.stream_name_label": "Stream Name",
|
"radio.wizard.feature_discord": "Discord Notifications",
|
||||||
"radio.wizard.stream_name_placeholder": "My Radio",
|
"radio.wizard.feature_discord_desc": "Notifications when DJ goes live / song changes",
|
||||||
"radio.wizard.stream_name_hint": "A name for your radio stream (optional)",
|
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
|
||||||
"radio.wizard.azurecast_section": "AzureCast Server Configuration",
|
"radio.wizard.discord_webhook_hint": "Create a webhook in your Discord server channel.",
|
||||||
"radio.wizard.azurecast_base_url_label": "AzureCast Base URL",
|
"radio.wizard.test_title": "Test Connection",
|
||||||
"radio.wizard.azurecast_base_url_hint": "The base URL of your AzureCast server. Auto-detected if left empty.",
|
"radio.wizard.test_desc": "Click Test Connection to check if your stream and APIs are reachable.",
|
||||||
"radio.wizard.azurecast_station_id_label": "Station ID",
|
"radio.wizard.test_loading": "Testing connection...",
|
||||||
"radio.wizard.azurecast_station_id_hint": "The station ID in AzureCast (default: 1)",
|
"radio.wizard.test_prompt": "Click the button to test the connection.",
|
||||||
"radio.wizard.enable_now_playing": "Enable Now Playing",
|
"radio.wizard.test_button": "Test Connection",
|
||||||
"radio.wizard.now_playing_api_label": "Now Playing API URL",
|
"radio.wizard.test_retry": "Test Again",
|
||||||
"radio.wizard.now_playing_api_hint": "API endpoint that returns the current song. Usually auto-detected.",
|
"radio.wizard.settings_overview": "Settings Overview",
|
||||||
"radio.wizard.enable_listeners": "Enable Listeners Counter",
|
"radio.wizard.settings_overview_desc": "These are the settings that will be saved:",
|
||||||
"radio.wizard.listeners_api_label": "Listeners API URL",
|
"radio.wizard.install_confirm": "Are you sure you want to install the radio with these settings?",
|
||||||
"radio.wizard.listeners_api_hint": "API endpoint that returns the listener count.",
|
"radio.wizard.install_button": "Install Radio",
|
||||||
"radio.wizard.enable_current_dj": "Show Current DJ",
|
"radio.wizard.test_result_stream": "Stream Connection",
|
||||||
"radio.wizard.detected": "detected!",
|
"radio.wizard.test_result_now_playing": "Now Playing",
|
||||||
"radio.wizard.detected_desc": "API endpoints were automatically found and filled in.",
|
"radio.wizard.test_result_listeners": "Listeners",
|
||||||
"radio.wizard.not_detected": "No automatic detection",
|
"radio.wizard.status_success": "Success",
|
||||||
"radio.wizard.not_detected_desc": "Fill in the API URLs manually or skip this step.",
|
"radio.wizard.status_warning": "Warning",
|
||||||
"radio.wizard.section_community": "Community Features",
|
"radio.wizard.status_error": "Error",
|
||||||
"radio.wizard.feature_shouts": "Shouts",
|
"radio.wizard.status_skipped": "Skipped",
|
||||||
"radio.wizard.feature_shouts_desc": "Leave messages",
|
"radio.wizard.status_untested": "Not tested",
|
||||||
"radio.wizard.feature_applications": "DJ Applications",
|
"radio.wizard.content_type": "Content-Type",
|
||||||
"radio.wizard.feature_applications_desc": "Apply as DJ",
|
"radio.wizard.http_status": "HTTP Status",
|
||||||
"radio.wizard.feature_requests": "Song Requests",
|
"radio.wizard.song": "Song",
|
||||||
"radio.wizard.feature_requests_desc": "Request songs",
|
"radio.wizard.artist": "Artist",
|
||||||
"radio.wizard.section_display": "Display",
|
"radio.wizard.listeners": "Listeners",
|
||||||
"radio.wizard.feature_widget": "Radio Widget",
|
"radio.wizard.api_url": "API URL",
|
||||||
"radio.wizard.feature_widget_desc": "Mini player on the site",
|
"radio.wizard.test_stream_ok": "Stream is reachable! You can install the radio.",
|
||||||
"radio.wizard.feature_widget_global": "Widget Everywhere",
|
"radio.wizard.test_stream_fail": "Stream is not reachable. Check the URL and try again.",
|
||||||
"radio.wizard.feature_widget_global_desc": "Show on all pages",
|
"radio.wizard.test_not_run": "Not tested yet.",
|
||||||
"radio.wizard.widget_position_label": "Widget Position",
|
"radio.wizard.test_connection_fail": "Could not run test: ",
|
||||||
"radio.wizard.position_bottom_right": "Bottom Right",
|
"radio.wizard.error": "Error",
|
||||||
"radio.wizard.position_bottom_left": "Bottom Left",
|
"radio.wizard.unknown_error": "Unknown error"
|
||||||
"radio.wizard.position_top_right": "Top Right",
|
|
||||||
"radio.wizard.position_top_left": "Top Left",
|
|
||||||
"radio.wizard.section_gamification": "Gamification",
|
|
||||||
"radio.wizard.feature_points": "Points System",
|
|
||||||
"radio.wizard.feature_points_desc": "Earn points by listening",
|
|
||||||
"radio.wizard.feature_contests": "Contests",
|
|
||||||
"radio.wizard.feature_contests_desc": "Organize competitions",
|
|
||||||
"radio.wizard.feature_giveaways": "Giveaways",
|
|
||||||
"radio.wizard.feature_giveaways_desc": "Give away prizes",
|
|
||||||
"radio.wizard.section_integrations": "Integrations",
|
|
||||||
"radio.wizard.feature_discord": "Discord Notifications",
|
|
||||||
"radio.wizard.feature_discord_desc": "Notifications when DJ goes live / song changes",
|
|
||||||
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
|
|
||||||
"radio.wizard.discord_webhook_hint": "Create a webhook in your Discord server channel.",
|
|
||||||
"radio.wizard.test_title": "Test Connection",
|
|
||||||
"radio.wizard.test_desc": "Click Test Connection to check if your stream and APIs are reachable.",
|
|
||||||
"radio.wizard.test_loading": "Testing connection...",
|
|
||||||
"radio.wizard.test_prompt": "Click the button to test the connection.",
|
|
||||||
"radio.wizard.test_button": "Test Connection",
|
|
||||||
"radio.wizard.test_retry": "Test Again",
|
|
||||||
"radio.wizard.settings_overview": "Settings Overview",
|
|
||||||
"radio.wizard.settings_overview_desc": "These are the settings that will be saved:",
|
|
||||||
"radio.wizard.install_confirm": "Are you sure you want to install the radio with these settings?",
|
|
||||||
"radio.wizard.install_button": "Install Radio",
|
|
||||||
"radio.wizard.test_result_stream": "Stream Connection",
|
|
||||||
"radio.wizard.test_result_now_playing": "Now Playing",
|
|
||||||
"radio.wizard.test_result_listeners": "Listeners",
|
|
||||||
"radio.wizard.status_success": "Success",
|
|
||||||
"radio.wizard.status_warning": "Warning",
|
|
||||||
"radio.wizard.status_error": "Error",
|
|
||||||
"radio.wizard.status_skipped": "Skipped",
|
|
||||||
"radio.wizard.status_untested": "Not tested",
|
|
||||||
"radio.wizard.content_type": "Content-Type",
|
|
||||||
"radio.wizard.http_status": "HTTP Status",
|
|
||||||
"radio.wizard.song": "Song",
|
|
||||||
"radio.wizard.artist": "Artist",
|
|
||||||
"radio.wizard.listeners": "Listeners",
|
|
||||||
"radio.wizard.api_url": "API URL",
|
|
||||||
"radio.wizard.test_stream_ok": "Stream is reachable! You can install the radio.",
|
|
||||||
"radio.wizard.test_stream_fail": "Stream is not reachable. Check the URL and try again.",
|
|
||||||
"radio.wizard.test_not_run": "Not tested yet.",
|
|
||||||
"radio.wizard.test_connection_fail": "Could not run test: ",
|
|
||||||
"radio.wizard.error": "Error",
|
|
||||||
"radio.wizard.unknown_error": "Unknown error"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+1509
-1513
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1509
-1513
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1504
-1508
File diff suppressed because it is too large
Load Diff
Executable → Regular
+1509
-1513
File diff suppressed because it is too large
Load Diff
Executable → Regular
+308
-349
@@ -1,351 +1,310 @@
|
|||||||
{
|
{
|
||||||
"commandocentrum.live_status": "Live Status",
|
"commandocentrum.live_status": "Live Status",
|
||||||
"commandocentrum.live_status_desc": "Real-time hotel statistieken",
|
"commandocentrum.live_status_desc": "Real-time hotel statistieken",
|
||||||
"commandocentrum.online": "Online",
|
"commandocentrum.online": "Online",
|
||||||
"commandocentrum.emulator": "Emulator",
|
"commandocentrum.emulator": "Emulator",
|
||||||
"commandocentrum.database": "Database",
|
"commandocentrum.database": "Database",
|
||||||
"commandocentrum.load": "Load",
|
"commandocentrum.load": "Load",
|
||||||
"commandocentrum.server_info": "Server Informatie",
|
"commandocentrum.server_info": "Server Informatie",
|
||||||
"commandocentrum.server_info_desc": "Gedetailleerde server status",
|
"commandocentrum.server_info_desc": "Gedetailleerde server status",
|
||||||
"commandocentrum.php_laravel": "PHP & Laravel",
|
"commandocentrum.php_laravel": "PHP & Laravel",
|
||||||
"commandocentrum.memory_disk": "Memory & Disk",
|
"commandocentrum.memory_disk": "Memory & Disk",
|
||||||
"commandocentrum.memory": "Memory",
|
"commandocentrum.memory": "Memory",
|
||||||
"commandocentrum.disk": "Disk",
|
"commandocentrum.disk": "Disk",
|
||||||
"commandocentrum.uptime": "Uptime",
|
"commandocentrum.uptime": "Uptime",
|
||||||
"commandocentrum.system_health": "Systeem Gezondheid",
|
"commandocentrum.system_health": "Systeem Gezondheid",
|
||||||
"commandocentrum.system_health_desc": "Automatische systeem diagnostiek",
|
"commandocentrum.system_health_desc": "Automatische systeem diagnostiek",
|
||||||
"commandocentrum.refresh": "Vernieuwen",
|
"commandocentrum.refresh": "Vernieuwen",
|
||||||
"commandocentrum.healthy": "Gezond",
|
"commandocentrum.healthy": "Gezond",
|
||||||
"commandocentrum.warnings": "Waarschuwingen",
|
"commandocentrum.warnings": "Waarschuwingen",
|
||||||
"commandocentrum.errors": "Fouten",
|
"commandocentrum.errors": "Fouten",
|
||||||
"commandocentrum.system_status": "Systeem Status",
|
"commandocentrum.system_status": "Systeem Status",
|
||||||
"commandocentrum.critical_issues": "Kritieke Problemen",
|
"commandocentrum.critical_issues": "Kritieke Problemen",
|
||||||
"commandocentrum.hotel_status": "Hotel Status",
|
"commandocentrum.hotel_status": "Hotel Status",
|
||||||
"commandocentrum.hotel_status_desc": "Emulator en Nitro status",
|
"commandocentrum.hotel_status_desc": "Emulator en Nitro status",
|
||||||
"commandocentrum.hotel_alert": "Hotel Alert",
|
"commandocentrum.hotel_alert": "Hotel Alert",
|
||||||
"commandocentrum.hotel_alert_desc": "Stuur een bericht naar alle online gebruikers",
|
"commandocentrum.hotel_alert_desc": "Stuur een bericht naar alle online gebruikers",
|
||||||
"commandocentrum.send_alert": "Verstuur Alert",
|
"commandocentrum.send_alert": "Verstuur Alert",
|
||||||
"commandocentrum.alert_message_placeholder": "Typ hier je alert bericht...",
|
"commandocentrum.alert_message_placeholder": "Typ hier je alert bericht...",
|
||||||
"commandocentrum.emulator_logs": "Emulator Logs",
|
"commandocentrum.emulator_logs": "Emulator Logs",
|
||||||
"commandocentrum.emulator_logs_desc": "Live emulator log viewer",
|
"commandocentrum.emulator_logs_desc": "Live emulator log viewer",
|
||||||
"commandocentrum.emulator_control": "Emulator Control",
|
"commandocentrum.emulator_control": "Emulator Control",
|
||||||
"commandocentrum.emulator_control_desc": "Volledige emulator controle",
|
"commandocentrum.emulator_control_desc": "Volledige emulator controle",
|
||||||
"commandocentrum.start": "Start",
|
"commandocentrum.start": "Start",
|
||||||
"commandocentrum.stop": "Stop",
|
"commandocentrum.stop": "Stop",
|
||||||
"commandocentrum.restart": "Restart",
|
"commandocentrum.restart": "Restart",
|
||||||
"commandocentrum.check": "Check",
|
"commandocentrum.check": "Check",
|
||||||
"commandocentrum.version": "Versie",
|
"commandocentrum.version": "Versie",
|
||||||
"commandocentrum.service": "Service",
|
"commandocentrum.service": "Service",
|
||||||
"commandocentrum.status": "Status",
|
"commandocentrum.status": "Status",
|
||||||
"commandocentrum.emulator_updates": "Emulator Updates",
|
"commandocentrum.emulator_updates_desc": "Configureer en update de emulator",
|
||||||
"commandocentrum.emulator_updates_desc": "Configureer en update de emulator",
|
"commandocentrum.build": "Bouwen",
|
||||||
"commandocentrum.check_updates": "Check Updates",
|
"commandocentrum.save": "Opslaan",
|
||||||
"commandocentrum.build": "Bouwen",
|
"commandocentrum.github_url": "GitHub URL",
|
||||||
"commandocentrum.sql_updates": "SQL Updates",
|
"commandocentrum.jar_direct_url": "JAR Direct URL",
|
||||||
"commandocentrum.save": "Opslaan",
|
"commandocentrum.jar_path": "JAR Pad",
|
||||||
"commandocentrum.github_url": "GitHub URL",
|
"commandocentrum.source_repo": "Source Repo",
|
||||||
"commandocentrum.jar_direct_url": "JAR Direct URL",
|
"commandocentrum.source_path": "Source Pad",
|
||||||
"commandocentrum.jar_path": "JAR Pad",
|
"commandocentrum.branch": "Branch",
|
||||||
"commandocentrum.source_repo": "Source Repo",
|
"commandocentrum.db_host": "DB Host",
|
||||||
"commandocentrum.source_path": "Source Pad",
|
"commandocentrum.db_name": "DB Naam",
|
||||||
"commandocentrum.branch": "Branch",
|
"commandocentrum.service_name": "Service Naam",
|
||||||
"commandocentrum.db_host": "DB Host",
|
"commandocentrum.emulator_backups_desc": "Bekijk en herstel emulator backups",
|
||||||
"commandocentrum.db_name": "DB Naam",
|
"commandocentrum.restore": "Herstellen",
|
||||||
"commandocentrum.service_name": "Service Naam",
|
"commandocentrum.nitro_client": "Nitro Client",
|
||||||
"commandocentrum.emulator_backups": "Emulator Backups",
|
"commandocentrum.clothing_sync": "Kleding Sync",
|
||||||
"commandocentrum.emulator_backups_desc": "Bekijk en herstel emulator backups",
|
"commandocentrum.clothing_sync_desc": "Sync catalogus kleding uit FigureMap",
|
||||||
"commandocentrum.no_backups": "Nog geen backups beschikbaar",
|
"commandocentrum.sync": "Sync",
|
||||||
"commandocentrum.backups_auto": "Backups worden automatisch aangemaakt bij elke emulator update",
|
"commandocentrum.clothing_items": "Kleding Items",
|
||||||
"commandocentrum.restore": "Herstellen",
|
"commandocentrum.notifications": "Meldingen",
|
||||||
"commandocentrum.nitro_client": "Nitro Client",
|
"commandocentrum.notifications_desc": "E-mail en Discord alerts",
|
||||||
"commandocentrum.nitro_client_desc": "Configureer en update Nitro",
|
"commandocentrum.test_discord": "Test Discord",
|
||||||
"commandocentrum.auto_detect": "Auto Detect",
|
"commandocentrum.email_notifications": "E-mail Meldingen",
|
||||||
"commandocentrum.generate_configs": "Genereer Configs",
|
"commandocentrum.email_address": "E-mail Adres",
|
||||||
"commandocentrum.client_path": "Client Pad",
|
"commandocentrum.discord_notifications": "Discord Meldingen",
|
||||||
"commandocentrum.renderer_path": "Renderer Pad",
|
"commandocentrum.webhook_url": "Webhook URL",
|
||||||
"commandocentrum.build_path": "Build Pad",
|
"commandocentrum.discord_ranks": "Ranks die Discord notificatie krijgen",
|
||||||
"commandocentrum.webroot": "Webroot",
|
"commandocentrum.discord_ranks_helper": "Laat leeg voor alleen staff (min_staff_rank)",
|
||||||
"commandocentrum.site_url": "Site URL",
|
"commandocentrum.social_login": "Social Login (v1.4)",
|
||||||
"commandocentrum.auto_updates": "Automatische Updates",
|
"commandocentrum.social_login_desc": "Enable social login providers",
|
||||||
"commandocentrum.auto_updates_desc": "Configureer automatische updates",
|
"commandocentrum.google_login": "Google Login",
|
||||||
"commandocentrum.enable_auto_updates": "Automatische Updates Inschakelen",
|
"commandocentrum.google_login_helper": "Allow users to login with Google",
|
||||||
"commandocentrum.schedule": "Schema (HH:MM)",
|
"commandocentrum.google_client_id": "Google Client ID",
|
||||||
"commandocentrum.days": "Dagen (0-6)",
|
"commandocentrum.google_client_id_helper": "From Google Cloud Console",
|
||||||
"commandocentrum.clothing_sync": "Kleding Sync",
|
"commandocentrum.google_client_secret": "Google Client Secret",
|
||||||
"commandocentrum.clothing_sync_desc": "Sync catalogus kleding uit FigureMap",
|
"commandocentrum.discord_login": "Discord Login",
|
||||||
"commandocentrum.sync": "Sync",
|
"commandocentrum.discord_login_helper": "Allow users to login with Discord",
|
||||||
"commandocentrum.clothing_items": "Kleding Items",
|
"commandocentrum.discord_client_id": "Discord Client ID",
|
||||||
"commandocentrum.notifications": "Meldingen",
|
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
|
||||||
"commandocentrum.notifications_desc": "E-mail en Discord alerts",
|
"commandocentrum.discord_client_secret": "Discord Client Secret",
|
||||||
"commandocentrum.test_discord": "Test Discord",
|
"commandocentrum.github_login": "GitHub Login",
|
||||||
"commandocentrum.email_notifications": "E-mail Meldingen",
|
"commandocentrum.github_login_helper": "Allow users to login with GitHub",
|
||||||
"commandocentrum.email_address": "E-mail Adres",
|
"commandocentrum.github_client_id": "GitHub Client ID",
|
||||||
"commandocentrum.discord_notifications": "Discord Meldingen",
|
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
|
||||||
"commandocentrum.webhook_url": "Webhook URL",
|
"commandocentrum.github_client_secret": "GitHub Client Secret",
|
||||||
"commandocentrum.discord_ranks": "Ranks die Discord notificatie krijgen",
|
"commandocentrum.staff_activity": "Staff Activity Log",
|
||||||
"commandocentrum.discord_ranks_helper": "Laat leeg voor alleen staff (min_staff_rank)",
|
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
|
||||||
"commandocentrum.update_history": "Update Geschiedenis",
|
"commandocentrum.recent_staff_activities": "Recent Staff Activities",
|
||||||
"commandocentrum.update_history_desc": "Laatste systeem updates",
|
"commandocentrum.last_20_actions": "Last 20 actions",
|
||||||
"commandocentrum.no_updates_found": "Geen updates gevonden",
|
"commandocentrum.no_staff_activities": "No staff activities recorded yet.",
|
||||||
"commandocentrum.social_login": "Social Login (v1.4)",
|
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
|
||||||
"commandocentrum.social_login_desc": "Enable social login providers",
|
"commandocentrum.error_loading_activities": "Error loading staff activities",
|
||||||
"commandocentrum.google_login": "Google Login",
|
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
|
||||||
"commandocentrum.google_login_helper": "Allow users to login with Google",
|
"commandocentrum.just_now": "Just now",
|
||||||
"commandocentrum.google_client_id": "Google Client ID",
|
"commandocentrum.minutes_ago": "m ago",
|
||||||
"commandocentrum.google_client_id_helper": "From Google Cloud Console",
|
"commandocentrum.hours_ago": "h ago",
|
||||||
"commandocentrum.google_client_secret": "Google Client Secret",
|
"commandocentrum.days_ago": "d ago",
|
||||||
"commandocentrum.discord_login": "Discord Login",
|
"commandocentrum.success": "Success",
|
||||||
"commandocentrum.discord_login_helper": "Allow users to login with Discord",
|
"commandocentrum.error": "Error",
|
||||||
"commandocentrum.discord_client_id": "Discord Client ID",
|
"commandocentrum.warning": "Warning",
|
||||||
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
|
"commandocentrum.info": "Info",
|
||||||
"commandocentrum.discord_client_secret": "Discord Client Secret",
|
"commandocentrum.emulator_started": "Emulator gestart!",
|
||||||
"commandocentrum.github_login": "GitHub Login",
|
"commandocentrum.emulator_start_failed": "Kon emulator niet starten",
|
||||||
"commandocentrum.github_login_helper": "Allow users to login with GitHub",
|
"commandocentrum.emulator_stopped": "Emulator gestopt!",
|
||||||
"commandocentrum.github_client_id": "GitHub Client ID",
|
"commandocentrum.emulator_stop_failed": "Kon emulator niet stoppen",
|
||||||
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
|
"commandocentrum.emulator_restarted": "Emulator herstart!",
|
||||||
"commandocentrum.github_client_secret": "GitHub Client Secret",
|
"commandocentrum.emulator_restart_failed": "Kon emulator niet herstarten",
|
||||||
"commandocentrum.staff_activity": "Staff Activity Log",
|
"commandocentrum.emulator_online": "Emulator is online en reageert!",
|
||||||
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
|
"commandocentrum.emulator_unreachable": "Emulator is niet bereikbaar via RCON",
|
||||||
"commandocentrum.recent_staff_activities": "Recent Staff Activities",
|
"commandocentrum.emulator_settings_saved": "Emulator instellingen opgeslagen!",
|
||||||
"commandocentrum.last_20_actions": "Last 20 actions",
|
"commandocentrum.alerts_saved": "Meldingen opgeslagen!",
|
||||||
"commandocentrum.no_staff_activities": "No staff activities recorded yet.",
|
"commandocentrum.test_sent": "Test bericht verzonden!",
|
||||||
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
|
"commandocentrum.webhook_empty": "Webhook URL is leeg",
|
||||||
"commandocentrum.error_loading_activities": "Error loading staff activities",
|
"commandocentrum.diagnostics_refreshed": "Diagnostiek vernieuwd",
|
||||||
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
|
"commandocentrum.unknown": "Unknown",
|
||||||
"commandocentrum.just_now": "Just now",
|
"commandocentrum.not_applicable": "N/B",
|
||||||
"commandocentrum.minutes_ago": "m ago",
|
"commandocentrum.offline": "Offline",
|
||||||
"commandocentrum.hours_ago": "h ago",
|
"commandocentrum.active": "Active",
|
||||||
"commandocentrum.days_ago": "d ago",
|
"commandocentrum.inactive": "Inactive",
|
||||||
"commandocentrum.success": "Success",
|
"commandocentrum.not_found": "Niet gevonden",
|
||||||
"commandocentrum.error": "Error",
|
"commandocentrum.ok": "OK",
|
||||||
"commandocentrum.warning": "Warning",
|
"commandocentrum.missing": "Ontbreekt",
|
||||||
"commandocentrum.info": "Info",
|
"commandocentrum.jars": "JARs",
|
||||||
"commandocentrum.emulator_started": "Emulator gestart!",
|
"commandocentrum.source": "Source",
|
||||||
"commandocentrum.emulator_start_failed": "Kon emulator niet starten",
|
"commandocentrum.method": "Methode",
|
||||||
"commandocentrum.emulator_stopped": "Emulator gestopt!",
|
"commandocentrum.jar_download_restart": "JAR Download & Herstart",
|
||||||
"commandocentrum.emulator_stop_failed": "Kon emulator niet stoppen",
|
"commandocentrum.maven_build_restart": "Maven Build & Herstart",
|
||||||
"commandocentrum.emulator_restarted": "Emulator herstart!",
|
"commandocentrum.manual_download": "Handmatig: Download JAR van GitHub",
|
||||||
"commandocentrum.emulator_restart_failed": "Kon emulator niet herstarten",
|
"commandocentrum.maven_pom": "Maven (pom.xml)",
|
||||||
"commandocentrum.emulator_online": "Emulator is online en reageert!",
|
"commandocentrum.no_pom": "Geen pom.xml",
|
||||||
"commandocentrum.emulator_unreachable": "Emulator is niet bereikbaar via RCON",
|
"commandocentrum.update_available": "Update beschikbaar",
|
||||||
"commandocentrum.building_emulator": "Emulator wordt gebouwd vanaf source...",
|
"commandocentrum.up_to_date": "Up-to-date",
|
||||||
"commandocentrum.emulator_built": "Emulator gebouwd!",
|
"commandocentrum.update": "Updaten",
|
||||||
"commandocentrum.build_failed": "Build mislukt",
|
"commandocentrum.rebuild": "Herbouwen",
|
||||||
"commandocentrum.configure_github_url": "Configureer eerst de Emulator GitHub URL",
|
"commandocentrum.latest": "Latest",
|
||||||
"commandocentrum.maven_not_installed": "Maven (mvn) is niet geïnstalleerd - kan niet bouwen",
|
"commandocentrum.remote": "Remote",
|
||||||
"commandocentrum.building_maven": "Building emulator with Maven...",
|
"commandocentrum.local": "Local",
|
||||||
"commandocentrum.build_success_jar": "Build succesvol! JAR verplaatst naar :jar. Herstart de emulator.",
|
"commandocentrum.client": "Client",
|
||||||
"commandocentrum.build_success": "Build succesvol! Herstart de emulator.",
|
"commandocentrum.renderer": "Renderer",
|
||||||
"commandocentrum.build_failed_logs": "Build mislukt - controleer logs",
|
"commandocentrum.webroot_status": "Webroot",
|
||||||
"commandocentrum.no_pom_xml": "Geen pom.xml gevonden - kan niet bouwen vanaf source",
|
"commandocentrum.rank": "Rank",
|
||||||
"commandocentrum.sql_applied": "SQL updates toegepast!",
|
"radio.title": "Radio",
|
||||||
"commandocentrum.update_complete": "Update controle voltooid",
|
"radio.music": "Muziek",
|
||||||
"commandocentrum.emulator_settings_saved": "Emulator instellingen opgeslagen!",
|
"radio.loading": "Laden...",
|
||||||
"commandocentrum.nitro_updated": "Nitro bijgewerkt! Build opnieuw met \"Build\" knop.",
|
"radio.navigation_label": "Radio",
|
||||||
"commandocentrum.nitro_up_to_date": "Nitro is al up-to-date!",
|
"radio.setup_page_title": "Radio Setup",
|
||||||
"commandocentrum.building_nitro": "Building Nitro...",
|
"radio.setup_page_subtitle": "Configureer je radio systeem in één keer",
|
||||||
"commandocentrum.nitro_build_success": "Nitro build succesvol!",
|
"radio.setup.success_title": "Radio Geïnstalleerd!",
|
||||||
"commandocentrum.nitro_build_warning": "Build gestart - controleer handmatig",
|
"radio.setup.success_body": "Radio systeem is succesvol geïnstalleerd en geconfigureerd!",
|
||||||
"commandocentrum.valid_url_required": "Voer een geldige URL in (bijv. https://epicnabbo.nl)",
|
"radio.setup.error_title": "Installatie Mislukt",
|
||||||
"commandocentrum.configs_generated": "Configs gegenereerd & bestaande instellingen behouden!",
|
"radio.setup.error_body": "Er is een fout opgetreden: :message",
|
||||||
"commandocentrum.config_generated_warning": "Config gegenereerd (controleer handmatig)",
|
"radio.setup.button_label": "Alles Installeren",
|
||||||
"commandocentrum.paths_detected": "Paths gedetecteerd en opgeslagen!",
|
"radio.setup.modal_heading": "Radio Installeren?",
|
||||||
"commandocentrum.nitro_settings_saved": "Nitro instellingen opgeslagen!",
|
"radio.setup.modal_description": "Dit zal alle radio instellingen configureren met standaard waarden.",
|
||||||
"commandocentrum.auto_update_saved": "Auto update instellingen opgeslagen!",
|
"radio.setup.modal_submit": "Ja, installeer!",
|
||||||
"commandocentrum.alerts_saved": "Meldingen opgeslagen!",
|
"radio.setup.tooltip": "Installeer het complete radio systeem",
|
||||||
"commandocentrum.test_sent": "Test bericht verzonden!",
|
"radio.setup_complete": "✅ Installatie Voltooid!",
|
||||||
"commandocentrum.webhook_empty": "Webhook URL is leeg",
|
"radio.what_gets_configured": "Wat wordt er geconfigureerd?",
|
||||||
"commandocentrum.diagnostics_refreshed": "Diagnostiek vernieuwd",
|
"radio.radio_stream": "Radio Stream",
|
||||||
"commandocentrum.unknown": "Unknown",
|
"radio.radio_stream_desc": "Stel je stream URL in met ondersteuning voor SHOUTcast, Icecast, AzureCast en andere streaming platforms.",
|
||||||
"commandocentrum.not_applicable": "N/B",
|
"radio.points_system": "Punten Systeem",
|
||||||
"commandocentrum.offline": "Offline",
|
"radio.points_system_desc": "Laat gebruikers punten verdienen door te luisteren, nummers aan te vragen en deel te nemen aan contests.",
|
||||||
"commandocentrum.active": "Active",
|
"radio.community_features": "Community Functies",
|
||||||
"commandocentrum.inactive": "Inactive",
|
"radio.community_features_desc": "Shouts, song requests, DJ aanmeldingen en meer community interacties.",
|
||||||
"commandocentrum.not_found": "Niet gevonden",
|
"radio.dj_management": "DJ Beheer",
|
||||||
"commandocentrum.ok": "OK",
|
"radio.dj_management_desc": "DJ ranks, schema, auto-detectie en Sambroadcaster/Virtual DJ integratie.",
|
||||||
"commandocentrum.missing": "Ontbreekt",
|
"radio.monitoring": "Stream Monitoring",
|
||||||
"commandocentrum.jars": "JARs",
|
"radio.monitoring_desc": "Houd je stream uptime in de gaten met real-time monitoring.",
|
||||||
"commandocentrum.source": "Source",
|
"radio.display_options": "Weergave Opties",
|
||||||
"commandocentrum.method": "Methode",
|
"radio.display_options_desc": "Widget, player stijlen, kleuren en aanpasbare CSS/JS.",
|
||||||
"commandocentrum.jar_download_restart": "JAR Download & Herstart",
|
"radio.default_settings": "Standaard Instellingen",
|
||||||
"commandocentrum.maven_build_restart": "Maven Build & Herstart",
|
"radio.radio_label": "Radio",
|
||||||
"commandocentrum.manual_download": "Handmatig: Download JAR van GitHub",
|
"radio.enabled": "Ingeschakeld",
|
||||||
"commandocentrum.maven_pom": "Maven (pom.xml)",
|
"radio.points_label": "Punten",
|
||||||
"commandocentrum.no_pom": "Geen pom.xml",
|
"radio.per_min": " per min",
|
||||||
"commandocentrum.update_available": "Update beschikbaar",
|
"radio.daily_limit": "Dagelijkse limiet",
|
||||||
"commandocentrum.up_to_date": "Up-to-date",
|
"radio.shouts_label": "Shouts",
|
||||||
"commandocentrum.update": "Updaten",
|
"radio.on": "Aan",
|
||||||
"commandocentrum.rebuild": "Herbouwen",
|
"radio.widget": "Widget",
|
||||||
"commandocentrum.latest": "Latest",
|
"radio.global": "Globaal",
|
||||||
"commandocentrum.remote": "Remote",
|
"radio.dj_apps": "DJ Aanmeldingen",
|
||||||
"commandocentrum.local": "Local",
|
"radio.open": "Open",
|
||||||
"commandocentrum.client": "Client",
|
"radio.monitoring_label": "Monitoring",
|
||||||
"commandocentrum.renderer": "Renderer",
|
"radio.contests_label": "Contesten",
|
||||||
"commandocentrum.webroot_status": "Webroot",
|
"radio.install_radio_system": "🚀 Radio Systeem Installeren",
|
||||||
"commandocentrum.rank": "Rank",
|
"radio.reset_settings": "Instellingen Resetten",
|
||||||
"radio.title": "Radio",
|
"radio.reset_confirm": "Weet je zeker dat je alle radio instellingen wilt resetten?",
|
||||||
"radio.music": "Muziek",
|
"radio.go_to_radio_settings": "Naar Radio Instellingen",
|
||||||
"radio.loading": "Laden...",
|
"radio.open_wizard": "🎯 Open Radio Wizard",
|
||||||
"radio.navigation_label": "Radio",
|
"radio.wizard_desc": "Stap-voor-stap wizard met verbindingstest",
|
||||||
"radio.setup_page_title": "Radio Setup",
|
"radio.wizard.title": "Radio Installatie Wizard",
|
||||||
"radio.setup_page_subtitle": "Configureer je radio systeem in één keer",
|
"radio.wizard.step_short": "Stap",
|
||||||
"radio.setup.success_title": "Radio Geïnstalleerd!",
|
"radio.wizard.step_prefix": "Stap",
|
||||||
"radio.setup.success_body": "Radio systeem is succesvol geïnstalleerd en geconfigureerd!",
|
"radio.wizard.of": "van",
|
||||||
"radio.setup.error_title": "Installatie Mislukt",
|
"radio.wizard.next_step": "Volgende Stap →",
|
||||||
"radio.setup.error_body": "Er is een fout opgetreden: :message",
|
"radio.wizard.previous_step": "← Vorige Stap",
|
||||||
"radio.setup.button_label": "Alles Installeren",
|
"radio.wizard.back_to_setup": "Terug naar setup",
|
||||||
"radio.setup.modal_heading": "Radio Installeren?",
|
"radio.wizard.step1_label": "Platform",
|
||||||
"radio.setup.modal_description": "Dit zal alle radio instellingen configureren met standaard waarden.",
|
"radio.wizard.step2_label": "Stream",
|
||||||
"radio.setup.modal_submit": "Ja, installeer!",
|
"radio.wizard.step3_label": "API",
|
||||||
"radio.setup.tooltip": "Installeer het complete radio systeem",
|
"radio.wizard.step4_label": "Functies",
|
||||||
"radio.setup_complete": "✅ Installatie Voltooid!",
|
"radio.wizard.step5_label": "Testen",
|
||||||
"radio.what_gets_configured": "Wat wordt er geconfigureerd?",
|
"radio.wizard.step1_subtitle": "Kies je streaming platform",
|
||||||
"radio.radio_stream": "Radio Stream",
|
"radio.wizard.step2_title": "Stream Configuratie",
|
||||||
"radio.radio_stream_desc": "Stel je stream URL in met ondersteuning voor SHOUTcast, Icecast, AzureCast en andere streaming platforms.",
|
"radio.wizard.step3_title": "API Configuratie",
|
||||||
"radio.points_system": "Punten Systeem",
|
"radio.wizard.step3_subtitle": "Now Playing & Luisteraars",
|
||||||
"radio.points_system_desc": "Laat gebruikers punten verdienen door te luisteren, nummers aan te vragen en deel te nemen aan contests.",
|
"radio.wizard.step4_title": "Functies Configureren",
|
||||||
"radio.community_features": "Community Functies",
|
"radio.wizard.step4_subtitle": "Kies welke radio functies je wilt inschakelen",
|
||||||
"radio.community_features_desc": "Shouts, song requests, DJ aanmeldingen en meer community interacties.",
|
"radio.wizard.step5_title": "Test & Installeren",
|
||||||
"radio.dj_management": "DJ Beheer",
|
"radio.wizard.step5_subtitle": "Controleer de verbinding en voltooi de installatie",
|
||||||
"radio.dj_management_desc": "DJ ranks, schema, auto-detectie en Sambroadcaster/Virtual DJ integratie.",
|
"radio.wizard.platform_shoutcast": "SHOUTcast",
|
||||||
"radio.monitoring": "Stream Monitoring",
|
"radio.wizard.platform_shoutcast_desc": "Geschikt voor SHOUTcast servers. Automatische detectie van nu afspelen en luisteraars via stats endpoint.",
|
||||||
"radio.monitoring_desc": "Houd je stream uptime in de gaten met real-time monitoring.",
|
"radio.wizard.platform_icecast": "Icecast",
|
||||||
"radio.display_options": "Weergave Opties",
|
"radio.wizard.platform_icecast_desc": "Geschikt voor Icecast servers. Gebruikt status-json.xsl voor automatische detectie.",
|
||||||
"radio.display_options_desc": "Widget, player stijlen, kleuren en aanpasbare CSS/JS.",
|
"radio.wizard.platform_azurecast": "AzureCast",
|
||||||
"radio.default_settings": "Standaard Instellingen",
|
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Volledige API integratie met now-playing, listeners en auto-configuratie.",
|
||||||
"radio.radio_label": "Radio",
|
"radio.wizard.platform_other": "Anders",
|
||||||
"radio.enabled": "Ingeschakeld",
|
"radio.wizard.platform_other_desc": "Een andere stream provider. Handmatige configuratie van stream URL en API endpoints.",
|
||||||
"radio.points_label": "Punten",
|
"radio.wizard.shoutcast_info_title": "SHOUTcast",
|
||||||
"radio.per_min": " per min",
|
"radio.wizard.shoutcast_info_desc": "Voer je SHOUTcast stream URL in. De wizard probeert automatisch de stats endpoint te vinden.",
|
||||||
"radio.daily_limit": "Dagelijkse limiet",
|
"radio.wizard.icecast_info_title": "Icecast",
|
||||||
"radio.shouts_label": "Shouts",
|
"radio.wizard.icecast_info_desc": "Voer je Icecast stream URL in. De wizard gebruikt status-json.xsl voor automatische detectie.",
|
||||||
"radio.on": "Aan",
|
"radio.wizard.azurecast_info_title": "AzureCast",
|
||||||
"radio.widget": "Widget",
|
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuratie. De wizard configureert alles via de AzureCast API.",
|
||||||
"radio.global": "Globaal",
|
"radio.wizard.other_info_title": "Andere Stream",
|
||||||
"radio.dj_apps": "DJ Aanmeldingen",
|
"radio.wizard.other_info_desc": "Voer je stream URL in. Je kunt later handmatig API endpoints configureren voor nu afspelen en luisteraars.",
|
||||||
"radio.open": "Open",
|
"radio.wizard.stream_url_label": "Stream URL *",
|
||||||
"radio.monitoring_label": "Monitoring",
|
"radio.wizard.stream_url_hint": "De directe URL naar je audiostreem (MP3, AAC, OGG, etc.)",
|
||||||
"radio.contests_label": "Contesten",
|
"radio.wizard.stream_name_label": "Stream Naam",
|
||||||
"radio.install_radio_system": "🚀 Radio Systeem Installeren",
|
"radio.wizard.stream_name_placeholder": "Mijn Radio",
|
||||||
"radio.reset_settings": "Instellingen Resetten",
|
"radio.wizard.stream_name_hint": "Een naam voor je radiostream (optioneel)",
|
||||||
"radio.reset_confirm": "Weet je zeker dat je alle radio instellingen wilt resetten?",
|
"radio.wizard.azurecast_section": "AzureCast Server Configuratie",
|
||||||
"radio.go_to_radio_settings": "Naar Radio Instellingen",
|
"radio.wizard.azurecast_base_url_label": "AzureCast Basis URL",
|
||||||
"radio.open_wizard": "🎯 Open Radio Wizard",
|
"radio.wizard.azurecast_base_url_hint": "De basis URL van je AzureCast server. Wordt automatisch gedetecteerd als leeg gelaten.",
|
||||||
"radio.wizard_desc": "Stap-voor-stap wizard met verbindingstest",
|
"radio.wizard.azurecast_station_id_label": "Station ID",
|
||||||
"radio.wizard.title": "Radio Installatie Wizard",
|
"radio.wizard.azurecast_station_id_hint": "Het station ID in AzureCast (standaard: 1)",
|
||||||
"radio.wizard.step_short": "Stap",
|
"radio.wizard.enable_now_playing": "Nu Afspelen inschakelen",
|
||||||
"radio.wizard.step_prefix": "Stap",
|
"radio.wizard.now_playing_api_label": "Now Playing API URL",
|
||||||
"radio.wizard.of": "van",
|
"radio.wizard.now_playing_api_hint": "API endpoint dat het huidige nummer teruggeeft. Meestal automatisch gedetecteerd.",
|
||||||
"radio.wizard.next_step": "Volgende Stap →",
|
"radio.wizard.enable_listeners": "Luisteraars teller inschakelen",
|
||||||
"radio.wizard.previous_step": "← Vorige Stap",
|
"radio.wizard.listeners_api_label": "Listeners API URL",
|
||||||
"radio.wizard.back_to_setup": "Terug naar setup",
|
"radio.wizard.listeners_api_hint": "API endpoint dat het aantal luisteraars teruggeeft.",
|
||||||
"radio.wizard.step1_label": "Platform",
|
"radio.wizard.enable_current_dj": "Huidige DJ tonen",
|
||||||
"radio.wizard.step2_label": "Stream",
|
"radio.wizard.detected": "gedetecteerd!",
|
||||||
"radio.wizard.step3_label": "API",
|
"radio.wizard.detected_desc": "API endpoints zijn automatisch gevonden en ingevuld.",
|
||||||
"radio.wizard.step4_label": "Functies",
|
"radio.wizard.not_detected": "Geen automatische detectie",
|
||||||
"radio.wizard.step5_label": "Testen",
|
"radio.wizard.not_detected_desc": "Vul de API URLs handmatig in of sla deze stap over.",
|
||||||
"radio.wizard.step1_subtitle": "Kies je streaming platform",
|
"radio.wizard.section_community": "Community Functies",
|
||||||
"radio.wizard.step2_title": "Stream Configuratie",
|
"radio.wizard.feature_shouts": "Shouts",
|
||||||
"radio.wizard.step3_title": "API Configuratie",
|
"radio.wizard.feature_shouts_desc": "Berichten achterlaten",
|
||||||
"radio.wizard.step3_subtitle": "Now Playing & Luisteraars",
|
"radio.wizard.feature_applications": "DJ Aanmeldingen",
|
||||||
"radio.wizard.step4_title": "Functies Configureren",
|
"radio.wizard.feature_applications_desc": "Solliciteren als DJ",
|
||||||
"radio.wizard.step4_subtitle": "Kies welke radio functies je wilt inschakelen",
|
"radio.wizard.feature_requests": "Song Verzoeken",
|
||||||
"radio.wizard.step5_title": "Test & Installeren",
|
"radio.wizard.feature_requests_desc": "Nummers aanvragen",
|
||||||
"radio.wizard.step5_subtitle": "Controleer de verbinding en voltooi de installatie",
|
"radio.wizard.section_display": "Weergave",
|
||||||
"radio.wizard.platform_shoutcast": "SHOUTcast",
|
"radio.wizard.feature_widget": "Radio Widget",
|
||||||
"radio.wizard.platform_shoutcast_desc": "Geschikt voor SHOUTcast servers. Automatische detectie van nu afspelen en luisteraars via stats endpoint.",
|
"radio.wizard.feature_widget_desc": "Miniplayer op de site",
|
||||||
"radio.wizard.platform_icecast": "Icecast",
|
"radio.wizard.feature_widget_global": "Widget Overal",
|
||||||
"radio.wizard.platform_icecast_desc": "Geschikt voor Icecast servers. Gebruikt status-json.xsl voor automatische detectie.",
|
"radio.wizard.feature_widget_global_desc": "Op alle pagina's tonen",
|
||||||
"radio.wizard.platform_azurecast": "AzureCast",
|
"radio.wizard.widget_position_label": "Widget Positie",
|
||||||
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Volledige API integratie met now-playing, listeners en auto-configuratie.",
|
"radio.wizard.position_bottom_right": "Rechtsonder",
|
||||||
"radio.wizard.platform_other": "Anders",
|
"radio.wizard.position_bottom_left": "Linksonder",
|
||||||
"radio.wizard.platform_other_desc": "Een andere stream provider. Handmatige configuratie van stream URL en API endpoints.",
|
"radio.wizard.position_top_right": "Rechtsboven",
|
||||||
"radio.wizard.shoutcast_info_title": "SHOUTcast",
|
"radio.wizard.position_top_left": "Linksboven",
|
||||||
"radio.wizard.shoutcast_info_desc": "Voer je SHOUTcast stream URL in. De wizard probeert automatisch de stats endpoint te vinden.",
|
"radio.wizard.section_gamification": "Gamification",
|
||||||
"radio.wizard.icecast_info_title": "Icecast",
|
"radio.wizard.feature_points": "Punten Systeem",
|
||||||
"radio.wizard.icecast_info_desc": "Voer je Icecast stream URL in. De wizard gebruikt status-json.xsl voor automatische detectie.",
|
"radio.wizard.feature_points_desc": "Verdien punten door te luisteren",
|
||||||
"radio.wizard.azurecast_info_title": "AzureCast",
|
"radio.wizard.feature_contests": "Contesten",
|
||||||
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuratie. De wizard configureert alles via de AzureCast API.",
|
"radio.wizard.feature_contests_desc": "Wedstrijden organiseren",
|
||||||
"radio.wizard.other_info_title": "Andere Stream",
|
"radio.wizard.feature_giveaways": "Giveaways",
|
||||||
"radio.wizard.other_info_desc": "Voer je stream URL in. Je kunt later handmatig API endpoints configureren voor nu afspelen en luisteraars.",
|
"radio.wizard.feature_giveaways_desc": "Cadeautjes weggeven",
|
||||||
"radio.wizard.stream_url_label": "Stream URL *",
|
"radio.wizard.section_integrations": "Integraties",
|
||||||
"radio.wizard.stream_url_hint": "De directe URL naar je audiostreem (MP3, AAC, OGG, etc.)",
|
"radio.wizard.feature_discord": "Discord Notificaties",
|
||||||
"radio.wizard.stream_name_label": "Stream Naam",
|
"radio.wizard.feature_discord_desc": "Meldingen bij DJ live / nummer wijziging",
|
||||||
"radio.wizard.stream_name_placeholder": "Mijn Radio",
|
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
|
||||||
"radio.wizard.stream_name_hint": "Een naam voor je radiostream (optioneel)",
|
"radio.wizard.discord_webhook_hint": "Maak een webhook aan in je Discord server kanaal.",
|
||||||
"radio.wizard.azurecast_section": "AzureCast Server Configuratie",
|
"radio.wizard.test_title": "Verbinding Testen",
|
||||||
"radio.wizard.azurecast_base_url_label": "AzureCast Basis URL",
|
"radio.wizard.test_desc": "Klik op Test Verbinding om te controleren of je stream en APIs bereikbaar zijn.",
|
||||||
"radio.wizard.azurecast_base_url_hint": "De basis URL van je AzureCast server. Wordt automatisch gedetecteerd als leeg gelaten.",
|
"radio.wizard.test_loading": "Bezig met testen van de verbinding...",
|
||||||
"radio.wizard.azurecast_station_id_label": "Station ID",
|
"radio.wizard.test_prompt": "Klik op de knop om de verbinding te testen.",
|
||||||
"radio.wizard.azurecast_station_id_hint": "Het station ID in AzureCast (standaard: 1)",
|
"radio.wizard.test_button": "Test Verbinding",
|
||||||
"radio.wizard.enable_now_playing": "Nu Afspelen inschakelen",
|
"radio.wizard.test_retry": "Opnieuw Testen",
|
||||||
"radio.wizard.now_playing_api_label": "Now Playing API URL",
|
"radio.wizard.settings_overview": "Overzicht van Instellingen",
|
||||||
"radio.wizard.now_playing_api_hint": "API endpoint dat het huidige nummer teruggeeft. Meestal automatisch gedetecteerd.",
|
"radio.wizard.settings_overview_desc": "Dit zijn de instellingen die worden opgeslagen:",
|
||||||
"radio.wizard.enable_listeners": "Luisteraars teller inschakelen",
|
"radio.wizard.install_confirm": "Weet je zeker dat je de radio wilt installeren met deze instellingen?",
|
||||||
"radio.wizard.listeners_api_label": "Listeners API URL",
|
"radio.wizard.install_button": "Radio Installeren",
|
||||||
"radio.wizard.listeners_api_hint": "API endpoint dat het aantal luisteraars teruggeeft.",
|
"radio.wizard.test_result_stream": "Stream Verbinding",
|
||||||
"radio.wizard.enable_current_dj": "Huidige DJ tonen",
|
"radio.wizard.test_result_now_playing": "Now Playing",
|
||||||
"radio.wizard.detected": "gedetecteerd!",
|
"radio.wizard.test_result_listeners": "Luisteraars",
|
||||||
"radio.wizard.detected_desc": "API endpoints zijn automatisch gevonden en ingevuld.",
|
"radio.wizard.status_success": "Succes",
|
||||||
"radio.wizard.not_detected": "Geen automatische detectie",
|
"radio.wizard.status_warning": "Waarschuwing",
|
||||||
"radio.wizard.not_detected_desc": "Vul de API URLs handmatig in of sla deze stap over.",
|
"radio.wizard.status_error": "Fout",
|
||||||
"radio.wizard.section_community": "Community Functies",
|
"radio.wizard.status_skipped": "Overgeslagen",
|
||||||
"radio.wizard.feature_shouts": "Shouts",
|
"radio.wizard.status_untested": "Niet getest",
|
||||||
"radio.wizard.feature_shouts_desc": "Berichten achterlaten",
|
"radio.wizard.content_type": "Content-Type",
|
||||||
"radio.wizard.feature_applications": "DJ Aanmeldingen",
|
"radio.wizard.http_status": "HTTP Status",
|
||||||
"radio.wizard.feature_applications_desc": "Solliciteren als DJ",
|
"radio.wizard.song": "Nummer",
|
||||||
"radio.wizard.feature_requests": "Song Verzoeken",
|
"radio.wizard.artist": "Artiest",
|
||||||
"radio.wizard.feature_requests_desc": "Nummers aanvragen",
|
"radio.wizard.listeners": "Luisteraars",
|
||||||
"radio.wizard.section_display": "Weergave",
|
"radio.wizard.api_url": "API URL",
|
||||||
"radio.wizard.feature_widget": "Radio Widget",
|
"radio.wizard.test_stream_ok": "Stream is bereikbaar! Je kunt de radio installeren.",
|
||||||
"radio.wizard.feature_widget_desc": "Miniplayer op de site",
|
"radio.wizard.test_stream_fail": "Stream is niet bereikbaar. Controleer de URL en probeer het opnieuw.",
|
||||||
"radio.wizard.feature_widget_global": "Widget Overal",
|
"radio.wizard.test_not_run": "Nog niet getest.",
|
||||||
"radio.wizard.feature_widget_global_desc": "Op alle pagina's tonen",
|
"radio.wizard.test_connection_fail": "Kon de test niet uitvoeren: ",
|
||||||
"radio.wizard.widget_position_label": "Widget Positie",
|
"radio.wizard.error": "Fout",
|
||||||
"radio.wizard.position_bottom_right": "Rechtsonder",
|
"radio.wizard.unknown_error": "Onbekende fout",
|
||||||
"radio.wizard.position_bottom_left": "Linksonder",
|
"Homepage": "Homepage",
|
||||||
"radio.wizard.position_top_right": "Rechtsboven",
|
"commandocentrum.nitro_update": "Nitro V3 Update",
|
||||||
"radio.wizard.position_top_left": "Linksboven",
|
"commandocentrum.nitro_update_desc": "Update alleen via command line — configureer instellingen in .env",
|
||||||
"radio.wizard.section_gamification": "Gamification",
|
"commandocentrum.nitro_cli_only": "Dit script kan alleen via de command line worden uitgevoerd. Gebruik: bash update-Nitrov3.sh\n\nInstellingen worden geconfigureerd in het .env bestand in de project root."
|
||||||
"radio.wizard.feature_points": "Punten Systeem",
|
|
||||||
"radio.wizard.feature_points_desc": "Verdien punten door te luisteren",
|
|
||||||
"radio.wizard.feature_contests": "Contesten",
|
|
||||||
"radio.wizard.feature_contests_desc": "Wedstrijden organiseren",
|
|
||||||
"radio.wizard.feature_giveaways": "Giveaways",
|
|
||||||
"radio.wizard.feature_giveaways_desc": "Cadeautjes weggeven",
|
|
||||||
"radio.wizard.section_integrations": "Integraties",
|
|
||||||
"radio.wizard.feature_discord": "Discord Notificaties",
|
|
||||||
"radio.wizard.feature_discord_desc": "Meldingen bij DJ live / nummer wijziging",
|
|
||||||
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
|
|
||||||
"radio.wizard.discord_webhook_hint": "Maak een webhook aan in je Discord server kanaal.",
|
|
||||||
"radio.wizard.test_title": "Verbinding Testen",
|
|
||||||
"radio.wizard.test_desc": "Klik op Test Verbinding om te controleren of je stream en APIs bereikbaar zijn.",
|
|
||||||
"radio.wizard.test_loading": "Bezig met testen van de verbinding...",
|
|
||||||
"radio.wizard.test_prompt": "Klik op de knop om de verbinding te testen.",
|
|
||||||
"radio.wizard.test_button": "Test Verbinding",
|
|
||||||
"radio.wizard.test_retry": "Opnieuw Testen",
|
|
||||||
"radio.wizard.settings_overview": "Overzicht van Instellingen",
|
|
||||||
"radio.wizard.settings_overview_desc": "Dit zijn de instellingen die worden opgeslagen:",
|
|
||||||
"radio.wizard.install_confirm": "Weet je zeker dat je de radio wilt installeren met deze instellingen?",
|
|
||||||
"radio.wizard.install_button": "Radio Installeren",
|
|
||||||
"radio.wizard.test_result_stream": "Stream Verbinding",
|
|
||||||
"radio.wizard.test_result_now_playing": "Now Playing",
|
|
||||||
"radio.wizard.test_result_listeners": "Luisteraars",
|
|
||||||
"radio.wizard.status_success": "Succes",
|
|
||||||
"radio.wizard.status_warning": "Waarschuwing",
|
|
||||||
"radio.wizard.status_error": "Fout",
|
|
||||||
"radio.wizard.status_skipped": "Overgeslagen",
|
|
||||||
"radio.wizard.status_untested": "Niet getest",
|
|
||||||
"radio.wizard.content_type": "Content-Type",
|
|
||||||
"radio.wizard.http_status": "HTTP Status",
|
|
||||||
"radio.wizard.song": "Nummer",
|
|
||||||
"radio.wizard.artist": "Artiest",
|
|
||||||
"radio.wizard.listeners": "Luisteraars",
|
|
||||||
"radio.wizard.api_url": "API URL",
|
|
||||||
"radio.wizard.test_stream_ok": "Stream is bereikbaar! Je kunt de radio installeren.",
|
|
||||||
"radio.wizard.test_stream_fail": "Stream is niet bereikbaar. Controleer de URL en probeer het opnieuw.",
|
|
||||||
"radio.wizard.test_not_run": "Nog niet getest.",
|
|
||||||
"radio.wizard.test_connection_fail": "Kon de test niet uitvoeren: ",
|
|
||||||
"radio.wizard.error": "Fout",
|
|
||||||
"radio.wizard.unknown_error": "Onbekende fout"
|
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+1503
-1507
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user