Compare commits

...

50 Commits

Author SHA1 Message Date
root 1db80e76fe chore: clean up repo structure and polish docs
- Rewrite .gitignore in English with proper patterns for build artifacts, storage files, and IDE files
- Remove tracked build assets (hash-based) and storage files from git
- Update LICENSE copyright to 2026 Remco (Epicnabbo)
- Fix clone URLs in README (remove placeholder gitea-server)
- Sync docs/INSTALL.md with README installation guide
2026-06-06 20:12:08 +02:00
root 51e3876ed9 fix: correct file permissions for www-data access 2026-06-06 19:40:48 +02:00
root 483342602f chore: update composer and npm dependencies to latest versions
- composer.json: 19 package version bumps (laravel/framework ^13.11,
  filament/filament ^5.6, pestphp/pest ^4.7, phpunit ^12.5, etc.)
- package.json: 22 package version bumps (eslint ^10, flowbite 4.0,
  vite ^8.0, tailwindcss 4.3, sass 1.100, etc.)
2026-06-06 19:24:34 +02:00
root ccbd200464 feat: auto-merge all Nitro config .example files with missing key detection 2026-06-06 16:38:48 +02:00
root 834908df9f fix: always sync Gamedata configs from latest .example files 2026-06-06 16:30:24 +02:00
root 434f58d5aa fix: use systemctl cat instead of list-units for reliable service detection 2026-06-06 16:24:44 +02:00
root 4c085ac7eb Move .env.install to docs/INSTALL.md as Markdown 2026-06-06 14:35:22 +02:00
root bbefdce290 Add safety note to update-NitroV3 tutorial 2026-06-06 14:31:44 +02:00
root 5b8d1cda7d Move CHANGELOG to docs/ and add English update-NitroV3 tutorial with troubleshooting 2026-06-06 14:29:54 +02:00
root ea2160132a Nitro V3 update: CLI-only (Linux), settings via .env
- Moved Nitro V3 update from web UI (Commandocentrum) to CLI-only
- Removed configure paths form and runUpdateNitrov3() from admin panel
- update-Nitrov3.sh now loads .env automatically from its directory
- Added all NITRO_* env vars to .env.example.linux and .env
- Removed configurable paths from database (replaced by .env)
- Updated README and CHANGELOG
2026-06-06 14:11:50 +02:00
root 2ac1264b93 Fix: SQL import exclude backup dir, replace process substitution with pipe, error handling in loop 2026-06-05 23:20:12 +02:00
root 0f0de43da5 Fix: mariadb-dump || echo instead of hard fail on missing tables 2026-06-05 23:17:03 +02:00
root 1ee60405ce Cleanup: clean_node_modules helper, yarn cache clean || true, remove duplicate blocks 2026-06-05 23:13:15 +02:00
root 78704190bc Fix: rm -rf node_modules fallback to sudo on permission denied + chown safety 2026-06-05 23:09:01 +02:00
root f109b1f764 Fix: check if jar exists after mvn package, clear error message 2026-06-05 23:03:47 +02:00
root af2f76d9db Fix: mariadb-dump --skip-lock-tables --force for missing tables 2026-06-05 23:01:59 +02:00
root 6f11cae3ca Fix: unbound variable _UNBUFFERED () 2026-06-05 23:00:45 +02:00
root 16b2f5bbb7 Fix: unbuffer for realtime output, set -euo pipefail + trap for 0 errors guarantee 2026-06-05 22:59:24 +02:00
root 6e437be6d1 Improve update-Nitrov3.sh: realtime output, strict yarn install, perms fix, extra validation 2026-06-05 22:56:20 +02:00
root 4f4f40ac99 Fix radio Filament: actions() must receive array not Closure 2026-06-04 21:04:28 +02:00
root be6a578f5e Fix radio Filament: recordActions->actions, toolbarActions->headerActions, EmbedCode statePath 2026-06-04 20:59:40 +02:00
root 4d8d22f40a Security: admin radio routes now require auth+admin.security, CORS default no longer wildcard, README security section 2026-06-04 20:46:07 +02:00
root 8a324b3082 README: security note on sudoers, restricted chown path, install polish 2026-06-04 20:36:20 +02:00
root f7fe86efeb Refactor HotelApiController into 6 focused controllers + FurniEditorController Eloquent migration 2026-06-04 20:32:15 +02:00
root 36887244e6 Fix config/app.php missing semicolon (caused 500) 2026-06-04 20:22:51 +02:00
root 9b5c655c68 High priority fixes: PayPal env(), RadioApiKey Bearer-only, User restrict, SettingsService TTL, PHPStan config, + fix 7 broke points (forceFill) 2026-06-04 20:17:45 +02:00
root b2bb1811d0 Medium priority fixes: CORS from env, shared HasRadioSettings trait, lazy RconService, validated() fixes, LogoGenerator hardening, DB indexes, user profile consistency, radio rank N+1 fix 2026-06-04 20:05:36 +02:00
root 4b6872e5e0 Low priority fixes: debug comments, Fortify cleanup, badge cost setting, profile query merge, User model fixes, VPN constructor cleanup, PayPal POST, PII removal, Dutch→English translations, duplicate rank check, CHANGELOG 2026-06-04 19:57:01 +02:00
root 66cbd46f37 Add 'What's New in V3' section to README 2026-06-04 19:41:48 +02:00
root 889cec60e9 Clean README in Arcturus style: badges, tables, quick start, clear sections 2026-06-04 19:37:10 +02:00
root 8ce3fcca85 Bulletproof installation: Redis, build-essential, DB user creation, PHP tuning, SSL, firewall, verification 2026-06-04 19:34:06 +02:00
root b8a15c8412 Compact and correct README: PHP 8.5, php8.5-fpm, Ubuntu 26.04, remove changelogs 2026-06-04 19:31:54 +02:00
root 82d00ad11d Rename .env.example to .env.install with step-by-step guide, update README 2026-06-04 19:30:24 +02:00
root c468040792 Add .env.example.linux and .env.example.windows, remove .env.standard, update README 2026-06-04 19:27:53 +02:00
root 4487084614 Update README: PHP 8.5, Ubuntu 26.04 2026-06-04 19:24:51 +02:00
root e96e2a0fd3 Remove all XAMPP support from CMS (files, references, configs) 2026-06-04 19:22:01 +02:00
root 4378144f45 Remove server IP from README, use placeholder 2026-06-04 19:19:02 +02:00
root 59188a5f2c XAMPP warning in English 2026-06-04 19:18:19 +02:00
root ff364992ca Add XAMPP security warning: not supported, extremely unsafe for production 2026-06-04 19:17:18 +02:00
root 2997486662 Rewrite README: document Nitro V3 update system, sudoers config, Commandocentrum; remove outdated content 2026-06-04 19:14:40 +02:00
root f76f30e88f Nitro V3 Update: configureerbare paths via Commandocentrum + sudoers fix 2026-06-04 18:56:08 +02:00
root 1f04979ffe Remove all auto-update functionality (commands, services, widgets, blades, translations) 2026-06-03 22:54:39 +02:00
root 1f9af5279a fix: add DEFAULT 0 to last_username_change column, seeder, fillable, and factory 2026-05-27 20:23:06 +02:00
root 7814176358 docs: update changelog (May 27, 2026) 2026-05-27 17:18:56 +02:00
root 56616d084c fix: add Dashboard/Homepage translations and OPcache troubleshooting to README 2026-05-27 17:16:47 +02:00
root e34300a8a1 Add /client/* to CORS paths 2026-05-26 19:15:50 +02:00
root e2422e9a16 Add CORS_ALLOWED_ORIGINS to .env.example 2026-05-26 19:02:58 +02:00
root a65db47c85 Add changelog to README 2026-05-26 17:25:01 +02:00
root af2c79ac59 Restore PHP 8.5 constraint (system runs PHP 8.5.6) 2026-05-26 17:09:30 +02:00
root e754858c84 Remove nonexistent PHP 8.5 constraint, restrict CORS allowed_headers 2026-05-26 17:08:31 +02:00
185 changed files with 32646 additions and 44162 deletions
-113
View File
@@ -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
+95
View File
@@ -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
+70
View File
@@ -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
-64
View File
@@ -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
-87
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+232 -383
View File
@@ -1,480 +1,329 @@
# AtomCMS — Remco Epicnabbo Edition # AtomCMS — Remco Epicnabbo Edition
<div align="center"> [![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/pP6HyZedAj)
<img src="https://i.imgur.com/9ePNdJ4.png" alt="Atom CMS" width="200"/> [![Laravel](https://img.shields.io/badge/Laravel-13.x-FF2D20?style=for-the-badge&logo=laravel&logoColor=white)](https://laravel.com)
[![PHP](https://img.shields.io/badge/PHP-8.5+-777BB4?style=for-the-badge&logo=php&logoColor=white)](https://php.net)
[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](#)
### 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** ---
[![Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/pP6HyZedAj) ## What's New in V3
[![Laravel](https://img.shields.io/badge/Laravel-13.x-FF2D20?style=flat&logo=laravel&logoColor=white)](https://laravel.com)
[![PHP](https://img.shields.io/badge/PHP-8.1+-777BB4?style=flat&logo=php&logoColor=white)](https://php.net)
[![Code Quality](https://img.shields.io/badge/Code%20Quality-A+-green?style=flat)](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>
-273
View File
@@ -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));
}
}
}
}
+1 -3
View File
@@ -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();
} }
} }
-150
View File
@@ -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");
}
}
}
-272
View File
@@ -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()];
}
}
}
-15
View File
@@ -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
+14 -326
View File
@@ -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 {
+13 -18
View File
@@ -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')
+21 -26
View File
@@ -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')
+1
View File
@@ -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')
+1 -1
View File
@@ -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
View File
@@ -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();
+2 -11
View File
@@ -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;
});
}
} }
+2 -12
View File
@@ -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,
+5 -5
View File
@@ -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);
} }
+1 -1
View File
@@ -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
View File
@@ -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));
}
}
-407
View File
@@ -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
+1 -1
View File
@@ -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();
} }
} }
+18 -5
View File
@@ -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;
} }
+2 -2
View File
@@ -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;
} }
-186
View File
@@ -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
View File
+24 -24
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -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
View File
+2 -2
View File
@@ -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' => [],
-5
View File
@@ -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
View File
@@ -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'),
+1
View File
@@ -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');
});
}
};
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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]);
}
}
-43
View File
@@ -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 "=========================================="
+40
View File
@@ -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
View File
@@ -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).
+404
View File
@@ -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
```
-397
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+305 -349
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1504 -1508
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+308 -349
View File
@@ -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
View File
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