670 lines
26 KiB
Markdown
670 lines
26 KiB
Markdown
# EpicNext-CMS v1.0.1
|
||
|
||
A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Drizzle ORM** and **React 19**. Designed to integrate seamlessly with Polaris / Arcturus Morningstar MySQL/MariaDB databases.
|
||
|
||
Features a premium animated homepage (typewriter hero, floating orbs, scroll counters), a full admin panel, NextAuth authentication (argon2id hashing with legacy md5/bcrypt auto-upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment.
|
||
|
||
---
|
||
|
||
## System Requirements
|
||
|
||
| Component | Version | Notes |
|
||
| --------------- | -------------- | ---------------------------------------- |
|
||
| Node.js | 26.7.0 | Current release pinned in `.nvmrc` |
|
||
| pnpm | >= 10.33.4 | Package manager (npm/yarn not supported) |
|
||
| MySQL / MariaDB | 8.0+ / 10.6+ | Shared with the emulator |
|
||
| DragonflyDB | 1.x+ | Optional — caching, rate limiting, SSE (Redis-protocol compatible) |
|
||
| Java | 17+ | Required only if building the emulator |
|
||
| Maven | 3.9+ | Required only if building the emulator |
|
||
|
||
---
|
||
|
||
## Quick Start
|
||
|
||
### 1. Clone & Install
|
||
|
||
```bash
|
||
git clone https://gitlab.epicnabbo.nl/remco/EpicNext-Cms.git
|
||
cd EpicNext-Cms
|
||
pnpm install
|
||
```
|
||
|
||
### 2. Database Setup
|
||
|
||
The CMS shares a database with the Polaris/Arcturus emulator. Use an existing database or create a new one:
|
||
|
||
```sql
|
||
CREATE DATABASE IF NOT EXISTS epicnext_cms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
```
|
||
|
||
The CMS reads emulator-owned tables (`users`, `items`, `rooms`, `bans`, etc.) directly. It never creates, alters, or drops them. The Drizzle schema in `src/db/schema.ts` is generated from the existing database structure and does not modify it.
|
||
|
||
> **Note:** The CMS does **not** own the emulator schema — it maps to those tables via Drizzle. Never run `drizzle-kit push` / `migrate` against the shared DB. CMS-owned tables (`website_*`, `radio_*`, etc.) are created via idempotent SQL in `drizzle/migrations/` (`pnpm db:migrate`).
|
||
|
||
### 3. Configure Environment
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
Edit `.env` with at minimum:
|
||
|
||
```dotenv
|
||
DATABASE_URL=mysql://user:[email protected]:3306/epicnext_cms
|
||
AUTH_SECRET=<random 32+ character string>
|
||
HOTEL_NAME=YourHotel
|
||
APP_URL=http://localhost:3000
|
||
```
|
||
|
||
See `.env.example` for all optional variables (RCON, email, DragonflyDB, OAuth, PayPal, etc.).
|
||
|
||
### 4. ORM Setup & Type Generation
|
||
|
||
#### Drizzle ORM (Primary Data Layer)
|
||
|
||
Drizzle ORM is the runtime data layer. The connection is a singleton in `src/lib/db.ts`:
|
||
|
||
```ts
|
||
import { db } from "@/lib/db";
|
||
import { users } from "@/db/schema";
|
||
import { eq } from "drizzle-orm/expressions";
|
||
|
||
const found = await db.select()
|
||
.from(users)
|
||
.where(eq(users.username, "hello"));
|
||
```
|
||
|
||
**Drizzle CLI** (`drizzle-kit`) is used for local development tasks — it is a devDependency and is never bundled in production.
|
||
|
||
| Command | What it does |
|
||
| ------- | ------------ |
|
||
| `pnpm db:generate` | Draft SQL from Drizzle schema into `drizzle/drafts/` (review + copy into `drizzle/migrations/`) |
|
||
| `pnpm db:studio` | Open Drizzle Studio (dev only) |
|
||
| `pnpm db:introspect` | Reverse-engineer an existing DB into a Drizzle schema draft |
|
||
| `pnpm db:schema:generate` | Regen committed `src/db/schema.ts` from previous schema names + live DB |
|
||
|
||
> The CMS does **not** use `drizzle-kit push` or `drizzle-kit migrate` — the database is shared with the emulator. Apply CMS DDL only via `pnpm db:migrate`.
|
||
|
||
Use `import { db } from "@/lib/db"` with table definitions from `src/db/schema.ts` for all database access. Types come from the committed Drizzle schema — no separate client code generation is required at build time.
|
||
|
||
### 5. Run CMS Migrations
|
||
|
||
```bash
|
||
pnpm db:migrate
|
||
```
|
||
|
||
Creates all CMS-owned tables (`website_*`, `radio_*`, `acl_*`, `admin_audit_log`, etc.) via idempotent SQL files in `drizzle/migrations/`. Emulator tables are never touched.
|
||
|
||
Check migration status:
|
||
|
||
```bash
|
||
pnpm db:migrate:status
|
||
```
|
||
|
||
### 6. Build & Start
|
||
|
||
```bash
|
||
# Development (hot reload)
|
||
pnpm dev
|
||
|
||
# Production
|
||
pnpm build && pnpm start
|
||
```
|
||
|
||
Open `http://localhost:3000` in your browser.
|
||
|
||
### 7. First Login
|
||
|
||
1. Register an account at `/register`, or log in with an existing emulator account.
|
||
2. Grant yourself admin access: `UPDATE users SET rank = 7 WHERE username = 'yourname';`
|
||
3. Visit `/admin` and configure your hotel via **Admin → CMS Settings**.
|
||
|
||
---
|
||
|
||
## DragonflyDB (caching, rate limiting, SSE)
|
||
|
||
DragonflyDB is a drop-in, Redis-compatible in-memory datastore. The CMS connects to it
|
||
via `REDIS_URL` using the `ioredis` client, so no application code changes are needed —
|
||
every Redis command (`PING`, `GET`, `SETEX`, `DEL`, `INCR`, `PEXPIRE`, `PTTL`) works
|
||
unchanged. It is optional: without it the CMS falls back to in-process memory.
|
||
|
||
### Install (Ubuntu/Debian)
|
||
|
||
```bash
|
||
curl -fsSL -o /tmp/dragonfly_amd64.deb \
|
||
https://github.com/dragonflydb/dragonfly/releases/download/v1.40.1/dragonfly_amd64.deb
|
||
apt-get install -y /tmp/dragonfly_amd64.deb
|
||
systemctl enable --now dragonfly
|
||
```
|
||
|
||
This installs a `dragonfly` systemd service and a config file at `/etc/dragonfly/dragonfly.conf`.
|
||
|
||
### Configure
|
||
|
||
```ini
|
||
--bind=127.0.0.1
|
||
--port=6379
|
||
--maxmemory=2gb
|
||
--version_check=false
|
||
```
|
||
|
||
- `--bind=127.0.0.1` keeps it private on the machine (matches `REDIS_URL=redis://127.0.0.1:6379`).
|
||
- `--port=6379` is the default Redis port, so `.env` stays unchanged.
|
||
- `--maxmemory` must be at least `0.25GiB` per CPU thread (e.g. `2gb` on a 6-thread server).
|
||
- `--version_check=false` disables the periodic outbound update check.
|
||
- Snapshots are written to `--dir` (`/var/lib/dragonfly/dump-*.dfs`).
|
||
|
||
### Point the CMS at it
|
||
|
||
```dotenv
|
||
REDIS_URL=redis://127.0.0.1:6379?connect_timeout=2
|
||
```
|
||
|
||
### Useful commands
|
||
|
||
```bash
|
||
redis-cli PING # → PONG
|
||
redis-cli FLUSHALL # clear the cache
|
||
systemctl status dragonfly # service health
|
||
```
|
||
|
||
Verify everything is wired up via the health endpoint:
|
||
`/api/health` should report `"redis": true`. The ioredis client automatically reconnects
|
||
after a DragonflyDB restart.
|
||
|
||
> **Note:** if an old Redis install still occupies port 6379, stop it first:
|
||
> `systemctl disable --now redis-server`.
|
||
|
||
## Nginx Configuration
|
||
|
||
The CMS is designed to run behind an nginx reverse proxy. Below is a reference configuration covering SSL termination, WebSocket upgrade, proxy caching, and the Habbo imager integration.
|
||
|
||
### Prerequisites
|
||
|
||
- SSL certificates in `/etc/ssl/cert.pem` and `/etc/ssl/key.pem` (or use Let's Encrypt)
|
||
- Next.js running on `127.0.0.1:3000` (default) or your configured port
|
||
- Habbo imager (optional) running on `127.0.0.1:3030`
|
||
|
||
### Reference Configuration
|
||
|
||
Create a file in `/etc/nginx/sites-available/epicnext` and symlink it to `sites-enabled`:
|
||
|
||
```nginx
|
||
# ==========================================
|
||
# GLOBAL SETTINGS
|
||
# ==========================================
|
||
server_tokens off;
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_proxied off;
|
||
gzip_comp_level 6;
|
||
gzip_min_length 256;
|
||
gzip_types text/plain text/css text/javascript application/json
|
||
application/javascript application/xml application/xml+rss
|
||
image/svg+xml font/opentype font/ttf font/woff font/woff2;
|
||
|
||
# ==========================================
|
||
# REDIRECT HTTP → HTTPS
|
||
# ==========================================
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name yourdomain.com www.yourdomain.com;
|
||
|
||
location /.well-known/acme-challenge/ {
|
||
root /var/www/epicnext/public;
|
||
}
|
||
|
||
location / {
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
}
|
||
|
||
# ==========================================
|
||
# MAIN HTTPS SERVER
|
||
# ==========================================
|
||
server {
|
||
listen 443 ssl;
|
||
listen [::]:443 ssl;
|
||
http2 on;
|
||
server_name yourdomain.com www.yourdomain.com;
|
||
|
||
root /var/www/epicnext/public;
|
||
index index.html;
|
||
|
||
# SSL Certificates
|
||
ssl_certificate /etc/ssl/cert.pem;
|
||
ssl_certificate_key /etc/ssl/key.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_prefer_server_ciphers off;
|
||
ssl_session_cache shared:SSL:10m;
|
||
ssl_session_timeout 1d;
|
||
ssl_session_tickets off;
|
||
|
||
# Security Headers
|
||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
|
||
client_max_body_size 20m;
|
||
client_body_timeout 30s;
|
||
client_header_timeout 10s;
|
||
keepalive_timeout 15s;
|
||
send_timeout 10s;
|
||
|
||
# Shared Proxy Settings
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_buffers 16 16k;
|
||
proxy_buffer_size 32k;
|
||
|
||
# ------------------------------------------
|
||
# Static Files
|
||
# ------------------------------------------
|
||
location ^~ /nitro-client/ {
|
||
alias /var/www/Nitro-V3/dist/;
|
||
expires 7d;
|
||
add_header Cache-Control "public";
|
||
access_log off;
|
||
}
|
||
|
||
location = /favicon.ico { expires 1y; access_log off; log_not_found off; try_files $uri =404; }
|
||
location = /robots.txt { expires 1d; access_log off; log_not_found off; try_files $uri =404; }
|
||
|
||
# ------------------------------------------
|
||
# Next.js Assets (immutable, long cache)
|
||
# ------------------------------------------
|
||
location /_next/static/ {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||
}
|
||
|
||
location /_next/data/ {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||
}
|
||
|
||
# ------------------------------------------
|
||
# API Routes (never cached)
|
||
# ------------------------------------------
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||
}
|
||
|
||
# ------------------------------------------
|
||
# Habbo Imager (optional)
|
||
# ------------------------------------------
|
||
# Proxies to a Docker container that renders Habbo avatars.
|
||
# The imager caches renders to disk, so a long s-maxage is safe.
|
||
location /imaging {
|
||
proxy_pass http://127.0.0.1:3030;
|
||
add_header Cache-Control "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400" always;
|
||
}
|
||
|
||
# ------------------------------------------
|
||
# WebSocket (Radio / SSE)
|
||
# ------------------------------------------
|
||
location /ws {
|
||
proxy_pass http://127.0.0.1:3030;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_read_timeout 86400;
|
||
}
|
||
|
||
# ------------------------------------------
|
||
# Main Page Proxy (with HTML caching)
|
||
# ------------------------------------------
|
||
# The CMS middleware sets:
|
||
# Cache-Control: public, s-maxage=300, stale-while-revalidate=300 (anonymous)
|
||
# Cache-Control: private, no-store (authenticated)
|
||
#
|
||
# nginx caches anonymous responses and serves them directly, bypassing
|
||
# the Node.js process entirely. Authenticated responses are never cached.
|
||
#
|
||
# proxy_cache_valid: cache 200 responses for 60 seconds
|
||
# proxy_ignore_headers Vary: Next.js emits many Vary headers (rsc,
|
||
# next-router-*, Accept-Encoding) that would fragment the cache key.
|
||
location / {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
|
||
proxy_http_version 1.1;
|
||
proxy_buffering on;
|
||
|
||
proxy_cache html_cache;
|
||
proxy_cache_valid 200 60s;
|
||
proxy_cache_key "$host$request_uri";
|
||
proxy_ignore_headers Vary;
|
||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||
proxy_cache_background_update on;
|
||
proxy_cache_revalidate on;
|
||
add_header X-Cache-Status $upstream_cache_status always;
|
||
}
|
||
|
||
# ------------------------------------------
|
||
# Health Check
|
||
# ------------------------------------------
|
||
location /health {
|
||
access_log off;
|
||
return 200 "OK";
|
||
add_header Content-Type text/plain;
|
||
}
|
||
|
||
# Block hidden files
|
||
location ~ /(\.|vendor|storage/logs/|\.(sql|sqlite|sqlite3)$) {
|
||
deny all;
|
||
access_log off;
|
||
log_not_found off;
|
||
}
|
||
}
|
||
```
|
||
|
||
### HTML Caching
|
||
|
||
The CMS uses an **origin-level proxy cache** for anonymous HTML pages. This means:
|
||
|
||
- **Anonymous visitors** receive cached HTML directly from nginx (~1ms), skipping the Node.js process entirely.
|
||
- **Authenticated visitors** always hit Node.js (personalized content).
|
||
- The cache is **auto-invalidated** after 60 seconds and revalidates in the background.
|
||
|
||
The proxy cache zone is defined in the `http` block (above any `server` block):
|
||
|
||
```nginx
|
||
proxy_cache_path /var/cache/nginx/html_cache levels=1:2 keys_zone=html_cache:50m max_size=500m inactive=10m use_temp_path=off;
|
||
```
|
||
|
||
Verify caching works by checking the `X-Cache-Status` response header:
|
||
|
||
```bash
|
||
# First request (MISS = fetched from Node.js, now cached)
|
||
curl -sI https://yourdomain.com/ | grep X-Cache-Status
|
||
# → X-Cache-Status: MISS
|
||
|
||
# Second request (HIT = served from nginx cache)
|
||
curl -sI https://yourdomain.com/ | grep X-Cache-Status
|
||
# → X-Cache-Status: HIT
|
||
```
|
||
|
||
### Key Points
|
||
|
||
| Setting | Value | Why |
|
||
| ------- | ----- | --- |
|
||
| `proxy_http_version 1.1` | HTTP/1.1 to upstream | Required for keep-alive and chunked transfer |
|
||
| `proxy_buffering on` | Buffer upstream response | Required for proxy_cache to work with chunked responses |
|
||
| `proxy_ignore_headers Vary` | Ignore upstream Vary | Next.js emits dynamic Vary headers (rsc, next-router-*) that would fragment the cache |
|
||
| `proxy_cache_valid 200 60s` | Cache 200s for 60s | Balances freshness with performance |
|
||
| `proxy_cache_use_stale` | Serve stale on error | Keeps the site available during brief upstream outages |
|
||
|
||
---
|
||
|
||
## Production Deployment (PM2)
|
||
|
||
```bash
|
||
pnpm build
|
||
pm2 start pnpm --name "next" -- start
|
||
pm2 save
|
||
```
|
||
|
||
Restart after updates:
|
||
|
||
```bash
|
||
git pull
|
||
pnpm install
|
||
pnpm build
|
||
pm2 restart next
|
||
```
|
||
|
||
The CMS runs behind an nginx reverse proxy on the default port 3000. Static assets (media uploads) are persisted via `/api/media/*` and survive rebuilds.
|
||
|
||
---
|
||
|
||
## Scripts
|
||
|
||
| Command | Description |
|
||
| ---------------------- | -------------------------------------------------- |
|
||
| `pnpm dev` | Start development server (hot reload) |
|
||
| `pnpm build` | Production build |
|
||
| `pnpm start` | Start production server |
|
||
| `pnpm typecheck` | Run TypeScript type checking |
|
||
| `pnpm test` | Run all tests (Vitest) |
|
||
| `pnpm db:migrate` | Apply pending SQL migrations |
|
||
| `pnpm db:migrate:status` | Show migration status |
|
||
| `pnpm db:schema:generate` | Regen `src/db/schema.ts` from prior schema + live DB |
|
||
| `pnpm db:generate` | Draft SQL via drizzle-kit → `drizzle/drafts/` |
|
||
| `pnpm db:studio` | Drizzle Studio (dev) |
|
||
| `pnpm db:introspect` | drizzle-kit introspect (draft) |
|
||
| `pnpm analyze` | Build + open bundle analyzer |
|
||
| `pnpm jobs:worker` | Start background task worker (systemd / PM2) |
|
||
| `pnpm biome:check` | Lint and format code |
|
||
|
||
**Drizzle Kit notes:** `db:generate` / `db:introspect` write drafts only. Reviewed SQL must be copied into `drizzle/migrations/` as a new numbered file, then applied with `pnpm db:migrate`. Never run `drizzle-kit push` or `drizzle-kit migrate` against production.
|
||
|
||
---
|
||
|
||
## Performance Features
|
||
|
||
| Feature | Description |
|
||
| ------------------------------ | -------------------------------------------------------------- |
|
||
| **React Compiler** | Automatic memoization — reduces unnecessary re-renders |
|
||
| **View Transitions API** | Native browser transitions between page navigations |
|
||
| **Lenis Smooth Scroll** | Fluid, customizable scrolling (respects `prefers-reduced-motion`) |
|
||
| **Server-Sent Events** | Real-time radio now-playing & listeners via SSE (no polling) |
|
||
| **DragonflyDB Caching** | Caches API responses (home, radio config) up to 30s in DragonflyDB |
|
||
| **Bundle Analyzer** | Run `pnpm analyze` to visualize and optimize bundle sizes |
|
||
| **RCON (TCP Socket)** | Live commands to the emulator (credits, badges, kick, ban) |
|
||
| **Streaming & Suspense** | Next.js App Router streaming for fast page loads |
|
||
| **Automatic Image Optimization** | `next/image` with Sharp for resizing and WebP/AVIF |
|
||
| **CSS-based Animations** | `input-glow`, `btn-shine`, `Reveal` scroll animations, floating orbs |
|
||
| **Compression** | Gzip compression enabled on all responses |
|
||
| **Stale Times** | Optimized router cache (30s dynamic, 180s static) |
|
||
| **PWA** | Service worker with skip-waiting, stale CSS chunk recovery |
|
||
| **Biome** | Fast linting and formatting (replaces ESLint + Prettier) |
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
├── drizzle/
|
||
│ ├── migrations/ # CMS SQL migrations (idempotent, tracked in cms_migrations)
|
||
│ └── drafts/ # drizzle-kit generate output (never auto-applied)
|
||
├── scripts/
|
||
│ ├── apply-migrations.ts # SQL migration runner (apply + status)
|
||
│ ├── jobs-worker.ts # Background task scheduler
|
||
│ ├── merge-config.cjs # Utility: merge split config files
|
||
│ └── generate-drizzle-schema.mjs # Regen src/db/schema.ts from schema + live DB
|
||
├── src/
|
||
│ ├── db/
|
||
│ │ ├── schema.ts # Drizzle ORM schema (committed — runtime data layer)
|
||
│ │ └── relations.ts # Drizzle relations
|
||
│ ├── app/ # Next.js App Router (pages & API routes)
|
||
│ ├── actions/ # Server Actions
|
||
│ ├── components/ # UI components
|
||
│ ├── lib/
|
||
│ │ ├── auth/ # NextAuth, password hashing, 2FA, SSO tickets
|
||
│ │ ├── services/ # RCON, email, currency, PayPal, alerts
|
||
│ │ ├── db.ts # Drizzle connection singleton (runtime)
|
||
│ │ ├── cached-db.ts # DragonflyDB-backed query cache helpers
|
||
│ │ ├── redis.ts # Cache client (ioredis → DragonflyDB)
|
||
│ │ ├── redis-cache.ts # Caching utility for API routes (uses DragonflyDB)
|
||
│ │ ├── cache.ts # In-memory cache fallback
|
||
│ │ ├── motion.ts # Framer Motion animation variants
|
||
│ │ └── use-event-source.ts # React hook for SSE subscriptions
|
||
│ ├── messages/ # i18n translations (en, nl, de, fr, es, it, pt, da, no, sv)
|
||
│ └── env.ts # Zod-validated environment schema
|
||
├── public/
|
||
│ ├── assets/ # Images, icons, fonts
|
||
│ └── scripts/ # Client-side scripts (theme-init.js)
|
||
├── update-Nitrov3.sh # Emulator & Nitro updater utility
|
||
├── next.config.ts # Next.js configuration
|
||
└── .env.example # Environment template with all options
|
||
```
|
||
|
||
### Database Ownership
|
||
|
||
| Component | Type | Migrations |
|
||
| ------------------------------------------------- | ----------------------- | ----------------------------------- |
|
||
| Emulator tables (`users`, `items`, `rooms`, etc.) | Existing Polaris schema | None — CMS reads/writes only |
|
||
| CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `drizzle/migrations/*.sql` |
|
||
| Migration tracking | `cms_migrations` table | Auto-created by migration runner |
|
||
|
||
### ORM Architecture
|
||
|
||
- **Runtime (Drizzle ORM)**: `@/lib/db` exposes a Drizzle singleton. Schema lives in `src/db/schema.ts`.
|
||
- **Schema regeneration**: `pnpm db:schema:generate` reuses field/table names from the previous `src/db/schema.ts` and refreshes column types from the live DB.
|
||
- **Drizzle Kit**: studio / generate / introspect for local tooling; CMS apply path remains `pnpm db:migrate`.
|
||
|
||
Use `import { db } from "@/lib/db"` with queries built via `src/db/schema.ts`.
|
||
|
||
---
|
||
|
||
## Optional Integrations
|
||
|
||
### Emulator — RCON
|
||
|
||
For live actions (credits, badges, rank changes, kick, ban, hotel alerts). The emulator must be running with RCON enabled at `RCON_HOST:RCON_PORT`.
|
||
|
||
### Client — Nitro
|
||
|
||
The `/client` page loads the Nitro client. Configure the client URL via **Admin → CMS Settings** (`nitro_client_url`). See `update-Nitrov3.sh` for deployment.
|
||
|
||
### Radio
|
||
|
||
Supports any streaming radio (e.g., Azuracast). Configure endpoints via CMS Settings:
|
||
|
||
- `radio_now_playing_api_url`
|
||
- `radio_listeners_api_url`
|
||
|
||
The radio player uses SSE for real-time updates (10s interval, no polling).
|
||
|
||
### Background Jobs
|
||
|
||
Run as a persistent process:
|
||
|
||
```bash
|
||
pnpm jobs:worker
|
||
```
|
||
|
||
Scheduled tasks:
|
||
- **Daily 03:00** — Emulator JAR backup (requires `EMULATOR_JAR_PATH` + `EMULATOR_BACKUP_DIR`)
|
||
- **Daily 04:00** — Cleanup login logs (>30 days) and expired password reset tokens (>7 days)
|
||
|
||
### CAPTCHA
|
||
|
||
Supports Cloudflare Turnstile and Google reCAPTCHA. Configure via CMS Settings.
|
||
|
||
### Theming
|
||
|
||
12 preset themes with fully customizable colors via **Admin → Theme** (`/admin/theme`).
|
||
|
||
|
||
## Furniture Import with Auto-Translation
|
||
|
||
The CMS now automatically translates furniture names and descriptions to **13 languages** on every import:
|
||
|
||
- **Native languages** (via official Habbo gamedata): Dutch (`nl`), English (`en`), German (`de`), French (`fr`), Spanish (`es`), Turkish (`tr`), Italian (`it`)
|
||
- **Additional languages** (via LibreTranslate self-hosted Docker at `127.0.0.1:5000`): Portuguese (`pt`), Finnish (`fi`), Polish (`pl`), Russian (`ru`), Arabic (`ar`), Japanese (`ja`)
|
||
|
||
### How it works
|
||
|
||
1. **Per-import translation** — Every import route (`/admin/import/furni`, batch, batch-regen, clone) triggers translation automatically after the furniture data is imported.
|
||
|
||
2. **Translation flow**:
|
||
- The original (usually English) `classname` and `description` are sent to LibreTranslate
|
||
- Official Habbo translations take priority for the 7 supported languages
|
||
- Custom/new meubels fall back to LibreTranslate
|
||
- Post-processing rules force Habbo‑style terminology (e.g. `bank` → `Bank`, `tafel` → `Tafel`, `stoel` → `Stoel`)
|
||
|
||
3. **No manual steps needed** — The translation happens as part of the import API calls. New custom furniture added via import immediately appears with translated names in the catalog for all 13 languages.
|
||
|
||
4. **CLI scripts** (optional):
|
||
- `pnpm translate:full` — translate all furniture data fully (uses `--full` flag)
|
||
- `pnpm translate:limited [--limit N] [--concurrency N]` — translate limited amount per language
|
||
- `pnpm build:languages [--full] [--limit N] [--concurrency N]` — build the full localized furnidata JSON files
|
||
|
||
### Requirements
|
||
|
||
- LibreTranslate Docker container running at `http://127.0.0.1:5000` (or configure a different URL in the environment)
|
||
- The `LIBRETRANSLATE_URL` environment variable can override the default if needed
|
||
- For the 7 official languages, no external service is needed — they use the built‑in Habbo gamedata
|
||
|
||
### Environment variables
|
||
|
||
```dotenv
|
||
# LibreTranslate endpoint (default: http://127.0.0.1:5000)
|
||
LIBRETRANSLATE_URL=http://127.0.0.1:5000
|
||
```
|
||
|
||
### Example import flow
|
||
|
||
```bash
|
||
# Single furniture import (triggers translation)
|
||
POST /admin/import/furni
|
||
|
||
# Batch import (triggers translation)
|
||
POST /admin/import/furni/batch
|
||
|
||
# Regenerate localized files
|
||
POST /admin/import/furni/batch-regen
|
||
|
||
# Clone import (triggers translation)
|
||
POST /admin/import/clone/batch
|
||
```
|
||
### AI Content Moderation
|
||
|
||
Optional OpenAI-powered moderation for comments and guestbook posts. Set `OPENAI_API_KEY`.
|
||
|
||
### FlareSolverr (Cloudflare Bypass)
|
||
|
||
Some clone sources (e.g. Leet, Hubbly, Habblet City) are protected by Cloudflare and return challenge pages instead of JSON. FlareSolverr acts as a proxy that solves these challenges automatically.
|
||
|
||
**Setup:**
|
||
|
||
```bash
|
||
docker compose up -d flaresolverr
|
||
```
|
||
|
||
Set the URL in `.env`:
|
||
|
||
```
|
||
FLARESOLVERR_URL=http://localhost:8191
|
||
```
|
||
|
||
When a source URL returns a 403 or HTML (CF challenge), the clone import automatically falls back to FlareSolverr. If FlareSolverr is not running or is unreachable, the source is skipped with a warning.
|
||
|
||
See `docker-compose.yml` for the FlareSolverr service definition.
|
||
|
||
---
|
||
|
||
## Development
|
||
|
||
```bash
|
||
pnpm dev # Start with hot reload
|
||
pnpm typecheck # Type check all files
|
||
pnpm test # Run test suite
|
||
pnpm analyze # Build and analyze bundle sizes
|
||
pnpm biome:check # Lint and format
|
||
```
|
||
|
||
### Contributing
|
||
|
||
1. Ensure typecheck and tests pass: `pnpm typecheck && pnpm test`
|
||
2. Follow existing code conventions (Server Components where possible, minimal client boundaries)
|
||
3. Use the `src/lib/motion.ts` animation variants for consistent animations
|
||
4. SQL migrations in `drizzle/migrations/` must be idempotent
|
||
5. For new database code, use the Drizzle runtime directly (`import { db } from "@/lib/db"`) — see [ORM Setup](#4-orm-setup--type-generation)
|
||
6. Avoid `any` — use `eslint-disable` or `biome-ignore` comments only when unavoidable
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
CC BY-NC-SA 4.0. See `LICENSE` for details.
|