Compare commits
55
Commits
v1.0.2
...
c433e5a52f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c433e5a52f | ||
|
|
540bce911f | ||
|
|
749dc237f1 | ||
|
|
8c193936f6 | ||
|
|
d3068ce88b | ||
|
|
340ecb42c8 | ||
|
|
39b211084d | ||
|
|
22162fa8b9 | ||
|
|
ae2b9328b9 | ||
|
|
84f64b6615 | ||
|
|
91357aca3b | ||
|
|
cc95a0d690 | ||
|
|
da390e447f | ||
|
|
4bd717d627 | ||
|
|
1311d36933 | ||
|
|
ded8fa62af | ||
|
|
3bf0644a9a | ||
|
|
d87c4284fe | ||
|
|
9cdb0b85fb | ||
|
|
7cdb785218 | ||
|
|
7f58e428ed | ||
|
|
a8d86a10bc | ||
|
|
b926ffa93c | ||
|
|
65fae257ba | ||
|
|
22a9fc13f7 | ||
|
|
3b853efba0 | ||
|
|
72079050f4 | ||
|
|
e08e366266 | ||
|
|
1e661a2b41 | ||
|
|
a637d09ca5 | ||
|
|
15eeab8a59 | ||
|
|
54fa1aecdd | ||
|
|
5140784dc7 | ||
|
|
41e41f0d22 | ||
|
|
46db76219f | ||
|
|
5b9e2166df | ||
|
|
738d7b8223 | ||
|
|
ab63de000b | ||
|
|
3532972357 | ||
|
|
e644362d09 | ||
|
|
b6b8625246 | ||
|
|
01126207dc | ||
|
|
9177230dc2 | ||
|
|
db39fb335c | ||
|
|
9ea67ecf72 | ||
|
|
15a76ffe84 | ||
|
|
9c0b339736 | ||
|
|
7384041bb6 | ||
|
|
a72646c933 | ||
|
|
a513d9b7bd | ||
|
|
3827f3e686 | ||
|
|
e408fdd5e6 | ||
|
|
78fab3c9ac | ||
|
|
e69c2fbb04 | ||
|
|
2e2aba11af |
No files matched your search
+43
-70
@@ -1,95 +1,68 @@
|
||||
# Connection to the LIVE/COPY emulator MySQL/MariaDB database.
|
||||
# The schema is owned by the Arcturus emulator — this app reads/writes data,
|
||||
# it does NOT own or migrate the emulator tables. Format:
|
||||
DATABASE_URL=mysql://user:[email protected]:3306/atomcms
|
||||
# ==============================================================================
|
||||
# Epicnextcms — Ultimate Speed & Low-Latency Example Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Optional pool tuning (defaults shown)
|
||||
DATABASE_POOL_SIZE=10
|
||||
DATABASE_IDLE_TIMEOUT_MS=300000
|
||||
DATABASE_CONNECT_TIMEOUT_MS=10000
|
||||
# --- DATABASE (High Performance Pooling & Strict Timeouts) ---
|
||||
DATABASE_URL="mysql://user:password@localhost:3306/dbname?charset=utf8mb4&connection_limit=150&connect_timeout=5"
|
||||
DATABASE_POOL_SIZE=150
|
||||
DATABASE_IDLE_TIMEOUT_MS=60000
|
||||
DATABASE_CONNECT_TIMEOUT_MS=5000
|
||||
|
||||
# Redis for rate limits, site-settings cache, and JWT invalidation
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
# --- REDIS (Lightning Fast Caching & Sessions) ---
|
||||
REDIS_URL=redis://127.0.0.1:6379?connect_timeout=2
|
||||
REDIS_CACHE_TTL_DEFAULT=7200
|
||||
|
||||
# Used by SSO ticket generation ({HOTEL_NAME}-{uuid})
|
||||
HOTEL_NAME=Atom
|
||||
APP_URL=http://localhost:3000
|
||||
# NEXT_PUBLIC_APP_URL=https://yourdomain.com
|
||||
AUTH_URL=http://localhost:3000
|
||||
# --- CORE RUNTIME & PERFORMANCE FLAGS ---
|
||||
NODE_ENV=production
|
||||
PORT=3002
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
UV_THREADPOOL_SIZE=16
|
||||
|
||||
# NextAuth — required in production (>=32 chars). Optional in development.
|
||||
# Laravel APP_KEY (base64:...) for existing 2FA secrets.
|
||||
AUTH_SECRET=
|
||||
APP_KEY=
|
||||
CONVERT_PASSWORDS=false
|
||||
# --- HOTEL & URLS ---
|
||||
HOTEL_NAME=EPIC WEB CONTROL
|
||||
APP_URL=http://localhost:3002
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3002
|
||||
AUTH_URL=http://localhost:3002
|
||||
|
||||
# Password hashing for NEW/upgraded passwords: "bcrypt" (default; 60-char $2y$,
|
||||
# fits a varchar(64) users.password) or "argon2id" (~97 chars, needs a wider
|
||||
# column). Existing accounts in either format still verify on login.
|
||||
PASSWORD_HASH=bcrypt
|
||||
# --- IMAGER ---
|
||||
IMAGING_UPSTREAM_URL=http://127.0.0.1:3030/imaging
|
||||
NEXT_PUBLIC_IMAGER_URL=http://localhost:3002/imaging
|
||||
|
||||
# Filesystem directory the badge uploader (/admin/badges) writes <code>.gif into
|
||||
# — the emulator's badge image folder (e.g. .../assets/c_images/album1584).
|
||||
# Leave unset to disable badge uploads.
|
||||
BADGE_UPLOAD_DIR=
|
||||
# --- SECURITY & HASHING ---
|
||||
AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars
|
||||
APP_KEY=base64:your-app-key-here=
|
||||
CONVERT_PASSWORDS=true
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
# Emulator JAR backup job (jobs-worker, runs host-side). When both are set, the
|
||||
# worker copies the JAR daily into the backup dir, keeping the newest N.
|
||||
EMULATOR_JAR_PATH=
|
||||
EMULATOR_BACKUP_DIR=
|
||||
# --- PATHS ---
|
||||
BADGE_UPLOAD_DIR=./public/assets/images/badges
|
||||
EMULATOR_JAR_PATH=./emulator/Arcturus.jar
|
||||
EMULATOR_BACKUP_DIR=./backups/emulator
|
||||
EMULATOR_BACKUP_KEEP=7
|
||||
|
||||
# RCON link to the Arcturus emulator
|
||||
# --- RCON (Low Latency Loop) ---
|
||||
RCON_HOST=127.0.0.1
|
||||
RCON_PORT=3001
|
||||
RCON_TIMEOUT_MS=2000
|
||||
|
||||
# Public imager URL — overrides the default /imaging relative path.
|
||||
# Falls back to NEXT_PUBLIC_APP_URL/imaging when only the app URL is set.
|
||||
# NEXT_PUBLIC_IMAGER_URL=https://epicnabbo.nl/imaging
|
||||
|
||||
# Preferred email provider (HTTP API). Used before SMTP when set.
|
||||
RESEND_API_KEY=
|
||||
|
||||
# Optional SMTP fallback (password reset / alert emails)
|
||||
# --- EMAIL & NOTIFICATIONS ---
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
SMTP_FROM=[email protected]
|
||||
|
||||
# Optional alerting (jobs worker / alert service)
|
||||
# --- ALERTING & MONITORING ---
|
||||
DISCORD_WEBHOOK_URL=
|
||||
ALERT_EMAIL=
|
||||
|
||||
# Optional AI content moderation (user comments / guestbook).
|
||||
# When set, posts are checked against the OpenAI Moderations endpoint in
|
||||
# addition to the website_wordfilter blocklist. Fail-open if unset/erroring.
|
||||
# --- MODERATION & PAYMENTS ---
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Optional PayPal top-up (sandbox by default)
|
||||
PAYPAL_CLIENT_ID=
|
||||
PAYPAL_SECRET=
|
||||
PAYPAL_API=https://api-m.sandbox.paypal.com
|
||||
|
||||
# Redis — REQUIRED for production (shared rate limits, site-settings cache,
|
||||
# JWT session invalidation cache). Without REDIS_URL the app falls back to
|
||||
# in-process memory: limits reset on restart and do not work across instances.
|
||||
# Deploy logs a loud warning when this is unset in production.
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
|
||||
# Logging level (debug | info | warn | error). Defaults to 'info' in production,
|
||||
# 'debug' in development. Production logs use structured JSON via pino.
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Optional Sentry error monitoring (no-op when unset).
|
||||
# Server/edge use SENTRY_DSN; browser uses NEXT_PUBLIC_SENTRY_DSN.
|
||||
SENTRY_DSN=
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
# Optional source-map upload during CI builds (requires SENTRY_AUTH_TOKEN).
|
||||
SENTRY_ORG=
|
||||
SENTRY_PROJECT=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
# Optional release tag shown in Sentry (e.g. git sha).
|
||||
# Deploy sets APP_VERSION + NEXT_PUBLIC_APP_VERSION from git sha.
|
||||
APP_VERSION=
|
||||
NEXT_PUBLIC_APP_VERSION=
|
||||
# --- LOGGING & SENTRY (Zod-Proof Dummy URLs) ---
|
||||
LOG_LEVEL=error
|
||||
SENTRY_DSN=https://[email protected]/0
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/0
|
||||
@@ -1,8 +1,12 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -30,12 +34,14 @@ jobs:
|
||||
git checkout -f "${REF}"
|
||||
|
||||
export SKIP_ENV_VALIDATION=1
|
||||
export ARGON2_MEMORY_SIZE=1024
|
||||
export ARGON2_ITERATIONS=1
|
||||
export NODE_ENV=test
|
||||
export DATABASE_URL="mysql://test:test@localhost:3306/test?charset=utf8mb4"
|
||||
export AUTH_SECRET="ci-test-secret-key-that-is-long-enough"
|
||||
export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1"
|
||||
export BCRYPT_ROUNDS=4
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm prisma:generate
|
||||
pnpm biome:lint
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
echo "--- CI checks passed ---"
|
||||
echo "--- CI checks passed ---"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
echo '<a id="what-is-epicnext-cms"></a>'
|
||||
echo "## What is EpicNext-CMS?"
|
||||
echo ""
|
||||
echo "EpicNext-CMS is a full public-facing hotel website plus an administrative panel. It features NextAuth authentication (argon2id/bcrypt with MD5 upgrade), real-time RCON communication with the emulator, Server-Sent Events for live radio, smooth page transitions and extensive extensibility. Full documentation: https://gitlab.epicnabbo.nl/remco/EpicNext-Cms/src/branch/main/README.md"
|
||||
echo "EpicNext-CMS is a full public-facing hotel website plus an administrative panel. It features NextAuth authentication (bcrypt with MD5 upgrade), real-time RCON communication with the emulator, Server-Sent Events for live radio, smooth page transitions and extensive extensibility. Full documentation: https://gitlab.epicnabbo.nl/remco/EpicNext-Cms/src/branch/main/README.md"
|
||||
echo ""
|
||||
echo '<a id="system-requirements"></a>'
|
||||
echo "## System Requirements"
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
pnpm install --frozen-lockfile
|
||||
# prisma generate does not need a live DB connection.
|
||||
pnpm prisma:generate
|
||||
export ARGON2_MEMORY_SIZE=1024 ARGON2_ITERATIONS=1 BCRYPT_ROUNDS=4
|
||||
export BCRYPT_ROUNDS=4
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
# Validate production env (AUTH_SECRET, DATABASE_URL, …) during build.
|
||||
@@ -377,10 +377,39 @@ jobs:
|
||||
# Prisma client is gitignored — regenerate into live src/generated.
|
||||
pnpm prisma:generate
|
||||
|
||||
sudo chown -R www-data:www-data "${LIVE}" 2>/dev/null || true
|
||||
sudo chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${LIVE}" 2>/dev/null || true
|
||||
|
||||
echo "Starting PM2 (zero-downtime reload)..."
|
||||
pm2 reload next --update-env || pm2 start next --update-env
|
||||
# Next.js prefers an already-set process PORT over .env. PM2 may still
|
||||
# have PORT=3000 from an older start, while .env (and nginx) expect 3002.
|
||||
# Export PORT before start so the process matches health checks.
|
||||
DEPLOY_PORT="$(grep -E '^[[:space:]]*PORT=' "${LIVE}/.env" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
|
||||
DEPLOY_PORT="$(printf '%s' "${DEPLOY_PORT}" | tr -cd '0-9')"
|
||||
DEPLOY_PORT="${DEPLOY_PORT:-3002}"
|
||||
export PORT="${DEPLOY_PORT}"
|
||||
echo "PM2/health PORT=${PORT}"
|
||||
|
||||
free_tcp_port() {
|
||||
local port="$1"
|
||||
[ -n "${port}" ] || return 0
|
||||
if command -v fuser >/dev/null 2>&1; then
|
||||
fuser -k "${port}/tcp" 2>/dev/null || true
|
||||
elif command -v lsof >/dev/null 2>&1; then
|
||||
# Portable fallback when fuser is unavailable.
|
||||
lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | xargs -r kill -9 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Starting PM2 with a clean PORT=${PORT} listener..."
|
||||
cd "${LIVE}"
|
||||
# Cutover already stopped the app, but failed deploys can leave orphans
|
||||
# on 3002 (or an old PM2 env still bound to 3000).
|
||||
pm2 stop next --kill-timeout 10000 2>/dev/null || true
|
||||
free_tcp_port "${PORT}"
|
||||
free_tcp_port 3000
|
||||
sleep 1
|
||||
pm2 delete next 2>/dev/null || true
|
||||
PORT="${PORT}" pm2 start pnpm --name next -- start
|
||||
pm2 save 2>/dev/null || true
|
||||
|
||||
sleep 3
|
||||
if ! pm2 show next 2>/dev/null | grep -q 'online'; then
|
||||
@@ -389,22 +418,25 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for HTTP health check..."
|
||||
HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:3000/api/health}"
|
||||
echo "Waiting for HTTP health check on port ${PORT}..."
|
||||
HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:${PORT}/api/health}"
|
||||
HEALTH_OK=0
|
||||
for i in $(seq 1 15); do
|
||||
for i in $(seq 1 20); do
|
||||
BODY="$(curl -sf --max-time 5 "${HEALTH_URL}" 2>/dev/null || true)"
|
||||
if echo "${BODY}" | grep -q '"database":true'; then
|
||||
echo "Health OK (${HEALTH_URL})"
|
||||
HEALTH_OK=1
|
||||
break
|
||||
fi
|
||||
echo "Health attempt ${i}/15 failed, retrying..."
|
||||
echo "Health attempt ${i}/20 failed (body=${BODY:-<empty>}), retrying..."
|
||||
sleep 2
|
||||
done
|
||||
if [ "${HEALTH_OK}" != "1" ]; then
|
||||
echo "ERROR: Health check failed after deploy (${HEALTH_URL})" >&2
|
||||
echo "Last body: ${BODY:-<empty>}" >&2
|
||||
echo "Listeners on PORT ${PORT}:" >&2
|
||||
ss -tlnp 2>/dev/null | grep ":${PORT} " >&2 || netstat -tlnp 2>/dev/null | grep ":${PORT} " >&2 || true
|
||||
pm2 env 0 2>/dev/null | grep -E '^PORT=' >&2 || true
|
||||
pm2 logs next --lines 40 --nostream >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -11,9 +11,8 @@ jobs:
|
||||
- name: Self-hosted Renovate
|
||||
run: |
|
||||
set -e
|
||||
|
||||
# Zorg ervoor dat de cache-map lokaal bestaat vóór Docker start
|
||||
# Dit voorkomt dat Docker de map automatisch als 'root' aanmaakt
|
||||
|
||||
# Ensure cache dir exists before Docker starts
|
||||
mkdir -p /var/tmp/renovate-cache
|
||||
|
||||
docker run --rm \
|
||||
@@ -25,4 +24,4 @@ jobs:
|
||||
-e RENOVATE_CONFIG_FILE='{"extends":["config:recommended"]}' \
|
||||
-e LOG_LEVEL=info \
|
||||
-v /var/tmp/renovate-cache:/tmp/renovate-cache \
|
||||
ghcr.io/renovatebot/renovate:latest
|
||||
ghcr.io/renovatebot/renovate:latest
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm exec lint-staged
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Prisma 7** 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/bcrypt with MD5-to-argon2id upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment.
|
||||
Features a premium animated homepage (typewriter hero, floating orbs, scroll counters), a full admin panel, NextAuth authentication (bcrypt with MD5-to-bcrypt upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Admin panel", () => {
|
||||
test("admin login page redirects unauthenticated users", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
await expect(page).toHaveURL(/login/);
|
||||
});
|
||||
|
||||
test("admin page has login form", async ({ page }) => {
|
||||
await page.goto("/admin");
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Authentication flows", () => {
|
||||
test("login form validates required fields", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator("text=required")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("login with invalid credentials shows error", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await page.fill('input[name="username"]', "nonexistent");
|
||||
await page.fill('input[name="password"]', "wrongpassword");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator("text=invalid")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("register page has password confirmation field", async ({ page }) => {
|
||||
await page.goto("/register");
|
||||
await expect(page.locator('input[name="confirmPassword"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("register form validates password match", async ({ page }) => {
|
||||
await page.goto("/register");
|
||||
await page.fill('input[name="password"]', "Password123!");
|
||||
await page.fill('input[name="confirmPassword"]', "DifferentPass123!");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator("text=match|komen overeen|kloppen")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("homepage has title", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveTitle(/Magic Hotel|Atom/i);
|
||||
});
|
||||
|
||||
test("register page loads", async ({ page }) => {
|
||||
await page.goto("/register");
|
||||
await expect(page).toHaveTitle(/registreer|register|konto/i);
|
||||
});
|
||||
|
||||
test("login page loads", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await expect(page).toHaveTitle(/inloggen|login/i);
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Public navigation", () => {
|
||||
test("homepage loads with expected elements", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("nav")).toBeVisible();
|
||||
await expect(page.locator("footer")).toBeVisible();
|
||||
});
|
||||
|
||||
test("community page loads", async ({ page }) => {
|
||||
await page.goto("/community");
|
||||
await expect(page).toHaveTitle(/community/i);
|
||||
});
|
||||
|
||||
test("shop page loads", async ({ page }) => {
|
||||
await page.goto("/shop");
|
||||
await expect(page.locator("h1, h2").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("404 page for unknown routes", async ({ page }) => {
|
||||
const response = await page.goto("/this-page-does-not-exist");
|
||||
expect(response?.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
module.exports = {
|
||||
ci: {
|
||||
collect: {
|
||||
url: ["http://localhost:3000"],
|
||||
numberOfRuns: 3,
|
||||
},
|
||||
assert: {
|
||||
preset: "lighthouse:no-pwa",
|
||||
assertions: {
|
||||
"categories:performance": ["error", { minScore: 0.9 }],
|
||||
"categories:accessibility": ["error", { minScore: 0.9 }],
|
||||
"categories:best-practices": ["error", { minScore: 0.9 }],
|
||||
"categories:seo": ["error", { minScore: 0.8 }],
|
||||
"first-contentful-paint": ["error", { maxNumericValue: 2000 }],
|
||||
"largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
|
||||
"cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
+6
-1
@@ -40,6 +40,11 @@ const nextConfig: NextConfig = {
|
||||
// Disable Next.js telemetry
|
||||
productionBrowserSourceMaps: false,
|
||||
|
||||
// Enable TypeScript CLI integration for TypeScript 7 compatibility
|
||||
experimental: {
|
||||
useTypeScriptCli: true,
|
||||
},
|
||||
|
||||
// Add caching headers for static assets
|
||||
async headers() {
|
||||
return [
|
||||
@@ -94,4 +99,4 @@ export default withBA(
|
||||
},
|
||||
telemetry: false,
|
||||
}),
|
||||
);
|
||||
);
|
||||
+12
-33
@@ -11,19 +11,13 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"prisma:generate": "prisma generate",
|
||||
"typecheck": "tsc6 --noEmit --incremental false",
|
||||
"typecheck": "tsc --noEmit --incremental",
|
||||
"biome:check": "biome check --write .",
|
||||
"biome:lint": "biome lint .",
|
||||
"biome:format": "biome format --write .",
|
||||
"knip": "knip",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"lint": "eslint . --ext .ts,.tsx --max-warnings=50",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix --max-warnings=50",
|
||||
"lhci:collect": "lhci collect",
|
||||
"lhci:assert": "lhci assert",
|
||||
"lhci:server": "lhci server",
|
||||
"db:migrate": "tsx scripts/apply-migrations.ts",
|
||||
"db:migrate:status": "tsx scripts/apply-migrations.ts --status",
|
||||
"jobs:worker": "tsx scripts/jobs-worker.ts",
|
||||
@@ -31,8 +25,7 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"biome check --write",
|
||||
"eslint --fix --max-warnings=50"
|
||||
"biome check --write"
|
||||
],
|
||||
"*.{json,md,css,scss,html}": [
|
||||
"biome format --write"
|
||||
@@ -46,36 +39,33 @@
|
||||
"@hookform/resolvers": "^5.5.7",
|
||||
"@prisma/adapter-mariadb": "^7.9.1",
|
||||
"@prisma/client": "^7.9.1",
|
||||
"@sentry/nextjs": "^10.68.0",
|
||||
"@tanstack/react-virtual": "^3.14.8",
|
||||
"bcrypt": "^6.0.0",
|
||||
"@sentry/nextjs": "^10.69.0",
|
||||
"@tanstack/react-virtual": "^3.14.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"croner": "^10.0.1",
|
||||
"framer-motion": "^12.42.2",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"isomorphic-dompurify": "^3.20.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"lenis": "^1.3.25",
|
||||
"lucide-react": "^1.27.0",
|
||||
"lucide-react": "^1.28.0",
|
||||
"lzma": "^2.3.2",
|
||||
"motion": "^12.43.0",
|
||||
"music-metadata": "^11.14.0",
|
||||
"mysql2": "^3.23.2",
|
||||
"next": "^16.2.12",
|
||||
"next-auth": "5.0.0-beta.32",
|
||||
"next-intl": "^4.13.4",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"nodemailer": "^9.0.3",
|
||||
"otplib": "^13.4.1",
|
||||
"pino": "^10.3.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hook-form": "^7.83.0",
|
||||
"resend": "^6.18.1",
|
||||
"sanitize-html": "^2.17.6",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.35.3",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -85,37 +75,26 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.6",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@next/bundle-analyzer": "^16.2.12",
|
||||
"@next/eslint-plugin-next": "^16.2.12",
|
||||
"@playwright/test": "1.62.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/node": "^26.1.2",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-security": "^4.0.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"husky": "^9.1.7",
|
||||
"knip": "^6.29.0",
|
||||
"lint-staged": "^17.2.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"postcss": "^8.5.24",
|
||||
"postcss": "^8.5.25",
|
||||
"prisma": "^7.9.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsx": "^4.23.1",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "8.1.5",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "8.2.0",
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
use: {
|
||||
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "pnpm dev",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
Generated
+800
-3235
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,47 @@
|
||||
const fs = require('fs');
|
||||
const JSON5 = require('json5');
|
||||
|
||||
const exampleFile = process.argv[2];
|
||||
const targetFile = process.argv[3];
|
||||
|
||||
if (!exampleFile || !targetFile) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function deepMerge(target, source) {
|
||||
const result = { ...target };
|
||||
for (const key of Object.keys(source)) {
|
||||
if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||||
result[key] = deepMerge(target[key] || {}, source[key]);
|
||||
} else {
|
||||
if (!(key in result)) {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let example;
|
||||
try {
|
||||
const content = fs.readFileSync(exampleFile, 'utf-8');
|
||||
example = JSON5.parse(content);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let current = {};
|
||||
try {
|
||||
if (fs.existsSync(targetFile)) {
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
current = JSON5.parse(content);
|
||||
}
|
||||
} catch {
|
||||
current = {};
|
||||
}
|
||||
|
||||
const merged = deepMerge(current, example);
|
||||
|
||||
const isJson5 = targetFile.endsWith('.json5');
|
||||
const output = isJson5 ? JSON5.stringify(merged, null, 4) : JSON.stringify(merged, null, 4);
|
||||
fs.writeFileSync(targetFile, output + '\n');
|
||||
@@ -1,66 +0,0 @@
|
||||
# ===========================================================================
|
||||
# Polaris Emulator — reference config.ini
|
||||
# Copy this to /var/www/emulator/Emulator/target/config.ini and fill in the
|
||||
# values marked CHANGE_ME. The emulator reads this file on startup.
|
||||
# Full guide: https://github.com/duckietm/Complete-Retro-on-Ubuntu
|
||||
# ===========================================================================
|
||||
|
||||
# ---- Database configuration ----
|
||||
db.hostname=127.0.0.1
|
||||
db.port=3306
|
||||
db.database=habbo # DB name (shared with the CMS)
|
||||
db.username=root # Username
|
||||
db.password=CHANGE_ME # Password
|
||||
db.params=?characterEncoding=utf8&useSSL=false&serverTimezone=Europe/Amsterdam
|
||||
db.pool.minsize=25
|
||||
db.pool.maxsize=100
|
||||
db.pool.connection_timeout_ms=10000
|
||||
db.pool.idle_timeout_ms=600000
|
||||
db.pool.max_lifetime_ms=1800000
|
||||
db.pool.validation_timeout_ms=5000
|
||||
db.pool.leak_detection_ms=20000 # set to 0 to disable leak detection
|
||||
|
||||
# ---- Game configuration ----
|
||||
# Host IP. Use 0.0.0.0 in most cases. Use 127.0.0.1 for LAN only.
|
||||
game.host=0.0.0.0
|
||||
game.port=3000
|
||||
|
||||
# ---- RCON configuration ----
|
||||
# Leave host at 127.0.0.1 if the CMS runs on the same server as the emulator.
|
||||
rcon.host=127.0.0.1
|
||||
rcon.port=3001
|
||||
rcon.allowed=127.0.0.1;127.0.0.2
|
||||
|
||||
# ---- Nitro secure runtime assets (disabled by default) ----
|
||||
nitro.secure.assets.enabled=false
|
||||
nitro.secure.api.enabled=false
|
||||
nitro.secure.session_ttl_sec=900
|
||||
nitro.secure.config.root=
|
||||
nitro.secure.gamedata.root=
|
||||
# Set a persistent secret when using Cloudflare or multiple backend requests.
|
||||
nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||
|
||||
# ---- Login ----
|
||||
login.remember.enabled=true
|
||||
login.remember.duration.days=30
|
||||
# Optional: set a persistent remember-me JWT secret here.
|
||||
login.remember.jwt.secret=
|
||||
login.news.limit=5
|
||||
|
||||
# ---- Secure runtime ECDH session TTL in seconds ----
|
||||
# (kept for compatibility; unused when secure assets are disabled)
|
||||
|
||||
# ---- WebSockets (Nitro client) ----
|
||||
ws.enabled=true
|
||||
ws.host=0.0.0.0
|
||||
ws.port=2096
|
||||
# Header used to obtain the real client IP when behind a proxy
|
||||
# (usually X-Forwarded-For, or CF-Connecting-IP behind Cloudflare).
|
||||
ws.ip.header=X-Forwarded-For
|
||||
|
||||
# ---- Console / emulator_settings ----
|
||||
# Run these SQL statements ONCE, after importing the emulator database:
|
||||
# USE habbo;
|
||||
# UPDATE emulator_settings SET `value` = '0' WHERE `key` = 'console.mode';
|
||||
# UPDATE emulator_settings SET `value` = 'MY_DOMAIN.COM,*.MY_DOMAIN.COM,localhost,127.0.0.1' WHERE `key` = 'websockets.whitelist';
|
||||
# console.mode MUST be 0 and the whitelist MUST match your domain.
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Launcher for the Polaris emulator.
|
||||
# Place at /var/www/emulator/Emulator/target/emulator and chmod +x.
|
||||
# Update the JAR name to match the version built by `mvn clean package`.
|
||||
|
||||
file_name_emulator=emulator.log
|
||||
current_time=$(date "+%H%M_%d-%m-%Y")
|
||||
file_name=$file_name_emulator.$current_time
|
||||
|
||||
# Rotate the previous log
|
||||
mkdir -p /var/log/emu
|
||||
mv /var/log/emu/emulator.log /var/log/emu/$file_name 2>/dev/null || true
|
||||
|
||||
# -Xmx4096m = 4 GB. 1 GB=1024, 2 GB=2048, 3 GB=3072, 4 GB=4096
|
||||
java -Dfile.encoding=UTF8 -Xmx4096m \
|
||||
-jar /var/www/emulator/Emulator/target/Habbo-*-jar-with-dependencies.jar \
|
||||
>/var/log/emu/emulator.log
|
||||
@@ -1,25 +0,0 @@
|
||||
[Unit]
|
||||
Description=Habbo Emulator
|
||||
|
||||
# Make sure the database has started before the emulator can start
|
||||
After=mariadb.service
|
||||
Requires=mariadb.service
|
||||
|
||||
# If you want to run as a less privileged account, change User below.
|
||||
# Make sure that account has the right permissions on the working directory and target folders.
|
||||
[Service]
|
||||
User=root
|
||||
|
||||
# Working directory where config.ini and the JAR live
|
||||
WorkingDirectory=/var/www/emulator/Emulator/target
|
||||
|
||||
# The launcher script we created below. It is a shell wrapper that calls the JAR.
|
||||
ExecStart=/var/www/emulator/Emulator/target/emulator
|
||||
|
||||
SuccessExitStatus=143
|
||||
TimeoutStopSec=10
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"distObfuscationEnabled": false,
|
||||
"secureAssetsEnabled": false,
|
||||
"secureApiEnabled": false,
|
||||
"apiBaseUrl": "https://MY_DOMAIN:2096",
|
||||
"plainConfigBaseUrl": "https://MY_DOMAIN/configuration/",
|
||||
"plainGamedataBaseUrl": "https://MY_DOMAIN/gamedata/"
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"socket.url": "wss://MY_DOMAIN.COM:2096",
|
||||
"crypto.ws.enabled": false,
|
||||
"crypto.ws.signing.enabled": false,
|
||||
"crypto.ws.signing.public_key": "",
|
||||
"api.url": "https://MY_DOMAIN.COM:2096",
|
||||
"asset.url": "https://MY_DOMAIN.COM/gamedata",
|
||||
"image.library.url": "http://MY_DOMAIN.COM/gamedata/c_images/",
|
||||
"hof.furni.url": "https://MY_DOMAIN.COM",
|
||||
"images.url": "${asset.url}/images",
|
||||
"gamedata.url": "${asset.url}",
|
||||
"sounds.url": "${asset.url}/sounds/%sample%.mp3",
|
||||
"external.texts.url": [
|
||||
"${gamedata.url}/config/ExternalTexts.json?v=1",
|
||||
"${gamedata.url}/config/UITexts.json?v=1"
|
||||
],
|
||||
"external.samples.url": "${gamedata.url}/sounds/sound_machine_sample_%sample%.mp3",
|
||||
"furnidata.url": "${gamedata.url}/config/FurnitureData.json?v=1",
|
||||
"productdata.url": "${gamedata.url}/config/ProductData.json?v=1",
|
||||
"avatar.actions.url": "${gamedata.url}/config/HabboAvatarActions.json?v=1",
|
||||
"avatar.figuredata.url": "${gamedata.url}/config/FigureData.json?v=1",
|
||||
"avatar.figuremap.url": "${gamedata.url}/config/FigureMap.json?v=1",
|
||||
"avatar.effectmap.url": "${gamedata.url}/config/EffectMap.json?v=1",
|
||||
"avatar.asset.url": "${asset.url}/clothes/%libname%.nitro",
|
||||
"avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro",
|
||||
"furni.asset.url": "${asset.url}/furniture/%libname%.nitro",
|
||||
"furni.asset.icon.url": "http://MY_DOMAIN.COM/gamedata/icons/%libname%%param%_icon.png",
|
||||
"pet.asset.url": "${asset.url}/pets/%libname%.nitro",
|
||||
"generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro",
|
||||
"room.asset.url": "${asset.url}/room/%libname%/%libname%.json",
|
||||
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
|
||||
"badge.asset.group.url": "http://MY_DOMAIN.COM/habbo-imaging/badge/%badgedata%",
|
||||
"badge.asset.group.external.url": "",
|
||||
"badge.asset.grouparts.url": "https://MY_DOMAIN.COM/gamedata/badgeparts/badgepart_%part%.png",
|
||||
"furni.rotation.bounce.steps": 20,
|
||||
"furni.rotation.bounce.height": 0.0625,
|
||||
"room.color.skip.transition": false,
|
||||
"enable.avatar.arrow": false,
|
||||
"system.animation.fps": 60,
|
||||
"system.limits.fps": false,
|
||||
"system.dispatcher.log": false,
|
||||
"system.packet.log": false,
|
||||
"system.pong.manually": true,
|
||||
"system.pong.interval.ms": 20000,
|
||||
"room.color.skip.transition": true,
|
||||
"user.badges.group.slot.enabled": true,
|
||||
"timezone.settings": "Europe/Amsterdam",
|
||||
"login.screen.enabled": true,
|
||||
"login.endpoint": "${api.url}/api/auth/login",
|
||||
"login.register.endpoint": "${api.url}/api/auth/register",
|
||||
"login.forgot.endpoint": "${api.url}/api/auth/forgot-password",
|
||||
"login.logout.endpoint": "${api.url}/api/auth/logout",
|
||||
"login.health.endpoint": "${api.url}/api/auth/health",
|
||||
"login.check-email.endpoint": "${api.url}/api/auth/check-email",
|
||||
"login.check-username.endpoint": "${api.url}/api/auth/check-username",
|
||||
"login.room_templates.endpoint": "${api.url}/api/auth/room-templates",
|
||||
"login.remember.endpoint": "${api.url}/api/auth/remember",
|
||||
"login.server_key.endpoint": "${api.url}/api/auth/server-key",
|
||||
"login.sso-token.endpoint": "${api.url}/api/auth/sso-token",
|
||||
"login.refresh.endpoint": "${api.url}/api/auth/refresh",
|
||||
"badges.custom.list.endpoint": "${api.url}/api/badges/custom",
|
||||
"badges.custom.create.endpoint": "${api.url}/api/badges/custom",
|
||||
"badges.custom.update.endpoint": "${api.url}/api/badges/custom/%badgeId%",
|
||||
"badges.custom.delete.endpoint": "${api.url}/api/badges/custom/%badgeId%",
|
||||
"badges.custom.texts.endpoint": "${api.url}/api/badges/custom/texts",
|
||||
"account.change-password.endpoint": "${api.url}/api/auth/change-password",
|
||||
"account.change-email.endpoint": "${api.url}/api/auth/change-email",
|
||||
"account.change-username.endpoint": "${api.url}/api/auth/change-username",
|
||||
"login.health.method": "GET",
|
||||
"login.news.url": "${asset.url}/news/news.json",
|
||||
"login.turnstile.enabled": false,
|
||||
"login.turnstile.sitekey": "",
|
||||
"avatar.mandatory.libraries": ["bd:1", "li:0"],
|
||||
"avatar.mandatory.effect.libraries": ["dance.1", "dance.2", "dance.3", "dance.4"]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"image.library.notifications.url": "${image.library.url}notifications/%image%.png",
|
||||
"achievements.images.url": "${image.library.url}Quests/%image%.png",
|
||||
"camera.url": "https://MY_DOMAIN.COM/camera/photo",
|
||||
"thumbnails.url": "https://MY_DOMAIN.COM/camera/photo/thumb/%thumbnail%.png",
|
||||
"url.prefix": "",
|
||||
"habbopages.url": "/gamedata/habbopages/",
|
||||
"group.homepage.url": "${url.prefix}/groups/%groupid%/id",
|
||||
"guide.help.alpha.groupid": 0,
|
||||
"chat.viewer.height.percentage": 0.4,
|
||||
"widget.dimmer.colorwheel": false,
|
||||
"avatar.wardrobe.max.slots": 10,
|
||||
"user.badges.max.slots": 5,
|
||||
"camera.publish.disabled": false,
|
||||
"hc.disabled": false,
|
||||
"badge.descriptions.enabled": true,
|
||||
"motto.max.length": 38,
|
||||
"bot.name.max.length": 15,
|
||||
"wired.action.bot.talk.to.avatar.max.length": 64,
|
||||
"wired.action.bot.talk.max.length": 64,
|
||||
"wired.action.chat.max.length": 100,
|
||||
"wired.action.kick.from.room.max.length": 100,
|
||||
"wired.action.mute.user.max.length": 100,
|
||||
"game.center.enabled": false,
|
||||
"catalog.style.new": true,
|
||||
"show.google.ads": false,
|
||||
"loginview": {
|
||||
"images": {
|
||||
"background": "${asset.url}/c_images/reception/stretch_blue.png",
|
||||
"background.colour": "#6eadc8",
|
||||
"sun": "${asset.url}/c_images/reception/sun.png",
|
||||
"drape": "${asset.url}/c_images/reception/drape.png",
|
||||
"left": "${asset.url}/c_images/reception/ts.png",
|
||||
"right": "${asset.url}/c_images/reception/US_right.png",
|
||||
"right.repeat": "${asset.url}/c_images/reception/US_top_right.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,16 @@ function revalidateHelpCenterTicketPaths(ticketId: bigint) {
|
||||
const id = String(ticketId);
|
||||
revalidatePath("/admin/help-tickets");
|
||||
revalidatePath(`/admin/help-tickets/${id}`);
|
||||
revalidatePath("/mod/help-tickets");
|
||||
revalidatePath(`/mod/help-tickets/${id}`);
|
||||
revalidatePath("/help/tickets");
|
||||
revalidatePath(`/help/tickets/${id}`);
|
||||
}
|
||||
|
||||
const HELP_TICKET_EDIT = [PERMS.TICKETS_EDIT, PERMS.MOD_TICKETS_EDIT] as const;
|
||||
|
||||
export const replyHelpCenterTicket = adminAction(
|
||||
{ permission: PERMS.TICKETS_EDIT, schema: replyHelpCenterTicketSchema },
|
||||
{ permission: HELP_TICKET_EDIT, schema: replyHelpCenterTicketSchema },
|
||||
async (ctx) => {
|
||||
const ticketId = ctx.data.ticketId;
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
@@ -72,7 +76,7 @@ export const replyHelpCenterTicket = adminAction(
|
||||
);
|
||||
|
||||
export const closeHelpCenterTicket = adminAction(
|
||||
{ permission: PERMS.TICKETS_EDIT, schema: helpCenterTicketIdSchema },
|
||||
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
||||
async (ctx) => {
|
||||
const ticketId = ctx.data.ticketId;
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
@@ -104,7 +108,7 @@ export const closeHelpCenterTicket = adminAction(
|
||||
);
|
||||
|
||||
export const reopenHelpCenterTicket = adminAction(
|
||||
{ permission: PERMS.TICKETS_EDIT, schema: helpCenterTicketIdSchema },
|
||||
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
||||
async (ctx) => {
|
||||
const ticketId = ctx.data.ticketId;
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
|
||||
@@ -53,6 +53,7 @@ export default async function CommandoCentrum() {
|
||||
emulatorOnline,
|
||||
maintenance,
|
||||
activity,
|
||||
onlineUsersRaw,
|
||||
] = await Promise.all([
|
||||
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
|
||||
prisma.user.count().catch(() => -1),
|
||||
@@ -70,8 +71,30 @@ export default async function CommandoCentrum() {
|
||||
.catch(
|
||||
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
||||
),
|
||||
prisma.user
|
||||
.findMany({
|
||||
where: { online: "1" },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
lastOnline: true,
|
||||
},
|
||||
orderBy: { lastOnline: "desc" },
|
||||
take: 40,
|
||||
})
|
||||
.catch(() => []),
|
||||
]);
|
||||
|
||||
const onlineUsers = onlineUsersRaw.map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
look: u.look,
|
||||
roomId: null as number | null,
|
||||
roomName: null as string | null,
|
||||
lastActivity: u.lastOnline > 0 ? new Date(u.lastOnline * 1000) : new Date(),
|
||||
}));
|
||||
|
||||
const staffIds = Array.from(
|
||||
new Set(activity.map((a) => Number(a.userId))),
|
||||
).filter((n) => n > 0);
|
||||
@@ -134,7 +157,7 @@ export default async function CommandoCentrum() {
|
||||
{/* ── Online users widget ───────────────────────────────── */}
|
||||
<section className="admin-card mb-6">
|
||||
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
|
||||
<OnlineUsersWidget users={[]} isLoading={false} />
|
||||
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
|
||||
</section>
|
||||
|
||||
{/* ── Server info + diagnostics ──────────────────────────── */}
|
||||
|
||||
@@ -42,10 +42,15 @@ export function AdminHelpTicketDetail({
|
||||
ticket,
|
||||
messages: initialMessages,
|
||||
canEdit,
|
||||
listHref = "/admin/help-tickets",
|
||||
hideRequesterContact = false,
|
||||
}: {
|
||||
ticket: HelpTicketInfo;
|
||||
messages: ThreadMessage[];
|
||||
canEdit: boolean;
|
||||
listHref?: string;
|
||||
/** Hide email/IP-style contact for mod-lite surfaces. */
|
||||
hideRequesterContact?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("pages.admin.helpTickets");
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
@@ -92,7 +97,7 @@ export function AdminHelpTicketDetail({
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Link href="/admin/help-tickets">
|
||||
<Link href={listHref}>
|
||||
<Button variant="ghost" size="icon" aria-label={t("backToList")}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -241,7 +246,11 @@ export function AdminHelpTicketDetail({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<Link
|
||||
href={`/admin/users/show/${ticket.creator.id}`}
|
||||
href={
|
||||
hideRequesterContact
|
||||
? `/u/${encodeURIComponent(ticket.creator.username)}`
|
||||
: `/admin/users/show/${ticket.creator.id}`
|
||||
}
|
||||
className="flex items-center gap-2 text-sm font-medium hover:text-primary"
|
||||
>
|
||||
<User className="h-3.5 w-3.5" />
|
||||
@@ -250,7 +259,7 @@ export function AdminHelpTicketDetail({
|
||||
{t("rankLabel", { rank: ticket.creator.rank })}
|
||||
</Badge>
|
||||
</Link>
|
||||
{ticket.creator.mail ? (
|
||||
{!hideRequesterContact && ticket.creator.mail ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Mail className="h-3 w-3" />
|
||||
{ticket.creator.mail}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import { positiveBigInt } from "@/lib/api";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
@@ -6,8 +7,6 @@ import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const STAFF_RANK_THRESHOLD = 4;
|
||||
|
||||
export default async function AdminHelpTicketDetailPage({
|
||||
params,
|
||||
}: {
|
||||
@@ -70,6 +69,7 @@ export default async function AdminHelpTicketDetailPage({
|
||||
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
const openerId = ticket.userId;
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
|
||||
const messages = [
|
||||
{
|
||||
@@ -86,7 +86,7 @@ export default async function AdminHelpTicketDetailPage({
|
||||
...replies.map((r) => {
|
||||
const user = userById.get(r.userId);
|
||||
const isStaff =
|
||||
r.userId !== openerId && (user?.rank ?? 0) >= STAFF_RANK_THRESHOLD;
|
||||
r.userId !== openerId && (user?.rank ?? 0) >= minStaffRank;
|
||||
return {
|
||||
key: String(r.id),
|
||||
userId: r.userId,
|
||||
|
||||
@@ -19,8 +19,10 @@ export interface HelpTicketRow {
|
||||
|
||||
export function HelpTicketsTable({
|
||||
data,
|
||||
basePath = "/admin/help-tickets",
|
||||
}: {
|
||||
data: PaginatedResult<HelpTicketRow>;
|
||||
basePath?: string;
|
||||
}) {
|
||||
const t = useTranslations("pages.admin.helpTickets");
|
||||
|
||||
@@ -36,7 +38,7 @@ export function HelpTicketsTable({
|
||||
#{row.id}
|
||||
</span>
|
||||
<Link
|
||||
href={`/admin/help-tickets/${row.id}`}
|
||||
href={`${basePath}/${row.id}`}
|
||||
className="font-medium truncate hover:underline"
|
||||
>
|
||||
{row.title}
|
||||
|
||||
@@ -160,6 +160,20 @@ export default async function AdminHelpTicketsPage({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
||||
role="status"
|
||||
>
|
||||
<p className="m-0">{t("queueHint")}</p>
|
||||
<p className="m-0 mt-1">
|
||||
<a
|
||||
href="/admin/tickets"
|
||||
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("queueOtherCta")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||
|
||||
@@ -116,6 +116,20 @@ export default async function AdminTicketsPage({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div
|
||||
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
|
||||
role="status"
|
||||
>
|
||||
<p className="m-0">{t("queueHint")}</p>
|
||||
<p className="m-0 mt-1">
|
||||
<a
|
||||
href="/admin/help-tickets"
|
||||
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("queueOtherCta")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||
<StatusCard
|
||||
|
||||
@@ -33,22 +33,7 @@ export async function GET() {
|
||||
|
||||
const emulator = await rcon.send("ping", null).catch(() => false);
|
||||
|
||||
let smtp = null;
|
||||
if (env.SMTP_HOST) {
|
||||
const nodemailer = await import("nodemailer");
|
||||
const test = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
secure: env.SMTP_SECURE,
|
||||
auth: env.SMTP_USER
|
||||
? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD ?? "" }
|
||||
: undefined,
|
||||
});
|
||||
smtp = await test
|
||||
.verify()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
const resendAvailable = !!env.RESEND_API_KEY;
|
||||
|
||||
const degraded = !database || redisOk === false;
|
||||
return apiJson({
|
||||
@@ -56,7 +41,7 @@ export async function GET() {
|
||||
database,
|
||||
redis: redisOk,
|
||||
emulator,
|
||||
smtp,
|
||||
resend: resendAvailable,
|
||||
node: process.version,
|
||||
uptime: Math.round(process.uptime()),
|
||||
time: new Date().toISOString(),
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { headers } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import { ViewTransitions } from "next-view-transitions";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { PwaRegister } from "@/components/pwa-register";
|
||||
@@ -90,7 +90,7 @@ export default async function RootLayout({
|
||||
>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeVars nonce={nonce} />
|
||||
<ViewTransitions>{children}</ViewTransitions>
|
||||
{children}
|
||||
<PwaRegister />
|
||||
<SmoothScroll />
|
||||
<Toaster
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { AdminHelpTicketDetail } from "@/app/admin/help-tickets/[id]/admin-help-ticket-detail";
|
||||
import { requireModPermission } from "@/lib/admin/guard";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import { positiveBigInt } from "@/lib/api";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ModHelpTicketDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]);
|
||||
|
||||
const { session, permissions } = await getAdminContext();
|
||||
const { id } = await params;
|
||||
const ticketId = positiveBigInt(id);
|
||||
if (!ticketId) notFound();
|
||||
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
where: { id: ticketId },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
content: true,
|
||||
open: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!ticket) notFound();
|
||||
|
||||
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
|
||||
where: { ticketId: ticket.id },
|
||||
orderBy: { id: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
const authorIds = [
|
||||
...new Set([
|
||||
...(ticket.userId != null ? [ticket.userId] : []),
|
||||
...replies.map((r) => r.userId),
|
||||
]),
|
||||
];
|
||||
|
||||
const users =
|
||||
authorIds.length > 0
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: authorIds } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
rank: true,
|
||||
mail: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
const openerId = ticket.userId;
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
|
||||
const messages = [
|
||||
{
|
||||
key: "opening",
|
||||
userId: openerId ?? 0,
|
||||
username:
|
||||
openerId != null
|
||||
? (userById.get(openerId)?.username ?? `#${openerId}`)
|
||||
: "—",
|
||||
isStaff: false,
|
||||
content: ticket.content,
|
||||
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||
},
|
||||
...replies.map((r) => {
|
||||
const user = userById.get(r.userId);
|
||||
const isStaff =
|
||||
r.userId !== openerId && (user?.rank ?? 0) >= minStaffRank;
|
||||
return {
|
||||
key: String(r.id),
|
||||
userId: r.userId,
|
||||
username: user?.username ?? `#${r.userId}`,
|
||||
isStaff,
|
||||
content: r.content,
|
||||
createdAt: (r.createdAt ?? new Date()).toISOString(),
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
const creator = openerId != null ? userById.get(openerId) : undefined;
|
||||
|
||||
const canEdit =
|
||||
canAccess(permissions, PERMS.MOD_TICKETS_EDIT, session.user.rank) ||
|
||||
canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||
|
||||
return (
|
||||
<AdminHelpTicketDetail
|
||||
ticket={{
|
||||
id: String(ticket.id),
|
||||
title: ticket.title,
|
||||
open: ticket.open,
|
||||
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
|
||||
creator: creator
|
||||
? {
|
||||
id: creator.id,
|
||||
username: creator.username,
|
||||
rank: creator.rank,
|
||||
mail: creator.mail ?? "",
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
messages={messages}
|
||||
canEdit={canEdit}
|
||||
listHref="/mod/help-tickets"
|
||||
hideRequesterContact
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
type HelpTicketRow,
|
||||
HelpTicketsTable,
|
||||
} from "@/app/admin/help-tickets/help-tickets-table";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { requireModPermission } from "@/lib/admin/guard";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ModHelpTicketsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) {
|
||||
await requireModPermission([PERMS.MOD_TICKETS_VIEW, PERMS.TICKETS_VIEW]);
|
||||
|
||||
const t = await getTranslations("pages.admin.helpTickets");
|
||||
const raw = await searchParams;
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status || "open";
|
||||
|
||||
const conditions: Prisma.WebsiteHelpCenterTicketsWhereInput[] = [];
|
||||
|
||||
if (statusFilter === "open") {
|
||||
conditions.push({ open: true });
|
||||
} else if (statusFilter === "closed") {
|
||||
conditions.push({ open: false });
|
||||
}
|
||||
|
||||
if (parsed.search.trim()) {
|
||||
const q = parsed.search.trim();
|
||||
const asId = /^\d+$/.test(q) ? BigInt(q) : null;
|
||||
const matchingUsers = await prisma.user
|
||||
.findMany({
|
||||
where: { username: { contains: q } },
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
conditions.push({
|
||||
OR: [
|
||||
{ title: { contains: q } },
|
||||
{ content: { contains: q } },
|
||||
...(matchingUsers.length > 0
|
||||
? [{ userId: { in: matchingUsers.map((u) => u.id) } }]
|
||||
: []),
|
||||
...(asId !== null ? [{ id: asId }] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.WebsiteHelpCenterTicketsWhereInput =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
|
||||
const SORT_MAP: Record<
|
||||
string,
|
||||
Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput
|
||||
> = {
|
||||
title: { title: "asc" },
|
||||
open: { open: "desc" },
|
||||
user: { userId: "asc" },
|
||||
date: { createdAt: "desc" },
|
||||
updated: { updatedAt: "desc" },
|
||||
id: { id: "desc" },
|
||||
};
|
||||
|
||||
const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" };
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput;
|
||||
|
||||
const [total, openCount, closedCount] = await Promise.all([
|
||||
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
|
||||
prisma.websiteHelpCenterTickets
|
||||
.count({ where: { open: true } })
|
||||
.catch(() => 0),
|
||||
prisma.websiteHelpCenterTickets
|
||||
.count({ where: { open: false } })
|
||||
.catch(() => 0),
|
||||
]);
|
||||
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const tickets = await prisma.websiteHelpCenterTickets
|
||||
.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
open: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
})
|
||||
.catch(() => []);
|
||||
|
||||
const ticketIds = tickets.map((ticket) => ticket.id);
|
||||
const userIds = [
|
||||
...new Set(
|
||||
tickets
|
||||
.map((ticket) => ticket.userId)
|
||||
.filter((id): id is number => id != null),
|
||||
),
|
||||
];
|
||||
|
||||
const [replyGroups, users] = await Promise.all([
|
||||
ticketIds.length > 0
|
||||
? prisma.websiteHelpCenterTicketReplies
|
||||
.groupBy({
|
||||
by: ["ticketId"],
|
||||
where: { ticketId: { in: ticketIds } },
|
||||
_count: true,
|
||||
})
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
userIds.length > 0
|
||||
? prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const replyCountByTicket = new Map(
|
||||
replyGroups.map((g) => [String(g.ticketId), g._count]),
|
||||
);
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
const rows: HelpTicketRow[] = tickets.map((ticket) => ({
|
||||
id: String(ticket.id),
|
||||
title: ticket.title,
|
||||
user:
|
||||
ticket.userId != null
|
||||
? (usernameById.get(ticket.userId) ?? `#${ticket.userId}`)
|
||||
: "—",
|
||||
userId: ticket.userId,
|
||||
replies: replyCountByTicket.get(String(ticket.id)) ?? 0,
|
||||
open: ticket.open,
|
||||
date: formatDate(ticket.createdAt, "date"),
|
||||
updated: formatDate(ticket.updatedAt, "date"),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||
</div>
|
||||
|
||||
{total === 0 && !parsed.search && statusFilter === "open" ? (
|
||||
<div className="admin-empty">{t("noTickets")}</div>
|
||||
) : (
|
||||
<HelpTicketsTable
|
||||
basePath="/mod/help-tickets"
|
||||
data={{
|
||||
rows,
|
||||
total,
|
||||
page: pagination.page,
|
||||
perPage: pagination.perPage,
|
||||
lastPage: pagination.lastPage,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+16
-1
@@ -1,4 +1,13 @@
|
||||
import { Ban, Gavel, LogOut, Radio, Shield, Ticket, Users } from "lucide-react";
|
||||
import {
|
||||
Ban,
|
||||
Gavel,
|
||||
LifeBuoy,
|
||||
LogOut,
|
||||
Radio,
|
||||
Shield,
|
||||
Ticket,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -59,6 +68,12 @@ export default async function ModLayout({ children }: { children: ReactNode }) {
|
||||
icon: Ticket,
|
||||
show: showTickets,
|
||||
},
|
||||
{
|
||||
href: "/mod/help-tickets",
|
||||
label: t("nav.helpTickets"),
|
||||
icon: LifeBuoy,
|
||||
show: showTickets,
|
||||
},
|
||||
{
|
||||
href: "/mod/actions",
|
||||
label: t("nav.actions"),
|
||||
|
||||
+59
-27
@@ -1,4 +1,12 @@
|
||||
import { Ban, Gavel, Radio, Shield, Ticket, Users } from "lucide-react";
|
||||
import {
|
||||
Ban,
|
||||
Gavel,
|
||||
LifeBuoy,
|
||||
Radio,
|
||||
Shield,
|
||||
Ticket,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatsCard } from "@/components/admin/stats-card";
|
||||
@@ -30,24 +38,30 @@ export default async function ModDashboardPage() {
|
||||
canAccess(permissions, PERMS.MOD_TEAM_VIEW, rank) ||
|
||||
canAccess(permissions, PERMS.MODERATION_VIEW, rank);
|
||||
|
||||
const [openCfh, todayBans, modsOnline, openTickets] = await Promise.all([
|
||||
showCfh
|
||||
? prisma.supportTickets.count({ where: { state: 0 } }).catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
showBans
|
||||
? prisma.ban
|
||||
.count({ where: { timestamp: { gte: todayStart } } })
|
||||
.catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
prisma.user
|
||||
.count({ where: { online: "1", rank: { gte: minStaffRank } } })
|
||||
.catch(() => 0),
|
||||
showTickets
|
||||
? prisma.websiteTicket
|
||||
.count({ where: { status: { not: "closed" } } })
|
||||
.catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
]);
|
||||
const [openCfh, todayBans, modsOnline, openTickets, openHelpTickets] =
|
||||
await Promise.all([
|
||||
showCfh
|
||||
? prisma.supportTickets.count({ where: { state: 0 } }).catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
showBans
|
||||
? prisma.ban
|
||||
.count({ where: { timestamp: { gte: todayStart } } })
|
||||
.catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
prisma.user
|
||||
.count({ where: { online: "1", rank: { gte: minStaffRank } } })
|
||||
.catch(() => 0),
|
||||
showTickets
|
||||
? prisma.websiteTicket
|
||||
.count({ where: { status: { not: "closed" } } })
|
||||
.catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
showTickets
|
||||
? prisma.websiteHelpCenterTickets
|
||||
.count({ where: { open: true } })
|
||||
.catch(() => 0)
|
||||
: Promise.resolve(0),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -75,6 +89,14 @@ export default async function ModDashboardPage() {
|
||||
href="/mod/tickets"
|
||||
/>
|
||||
) : null}
|
||||
{showTickets ? (
|
||||
<StatsCard
|
||||
title={t("stats.openHelpTickets")}
|
||||
value={openHelpTickets}
|
||||
icon={<LifeBuoy className="h-4 w-4" />}
|
||||
href="/mod/help-tickets"
|
||||
/>
|
||||
) : null}
|
||||
{showBans ? (
|
||||
<StatsCard
|
||||
title={t("stats.bansToday")}
|
||||
@@ -120,14 +142,24 @@ export default async function ModDashboardPage() {
|
||||
</li>
|
||||
) : null}
|
||||
{showTickets ? (
|
||||
<li>
|
||||
<Link
|
||||
href="/mod/tickets"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t("nav.tickets")}
|
||||
</Link>
|
||||
</li>
|
||||
<>
|
||||
<li>
|
||||
<Link
|
||||
href="/mod/tickets"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t("nav.tickets")}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/mod/help-tickets"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t("nav.helpTickets")}
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
{showActions ? (
|
||||
<li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { motion, useInView } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function AnimatedCounter({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const cmsDetails = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { type ReactNode, useRef, useState } from "react";
|
||||
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { pageTransition } from "@/lib/motion";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface RevealProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { dropdownVariants } from "@/lib/motion";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEventSource } from "@/lib/use-event-source";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function TypewriterText({
|
||||
|
||||
+1
-7
@@ -50,17 +50,11 @@ const schema = z
|
||||
// Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets.
|
||||
APP_KEY: z.string().optional(),
|
||||
|
||||
// Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->argon2id.
|
||||
// Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->bcrypt upgrade.
|
||||
CONVERT_PASSWORDS: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => v === "true" || v === "1"),
|
||||
// Hashing driver for NEW passwords: bcrypt (default, fits varchar(64)) | argon2id.
|
||||
PASSWORD_HASH: z.enum(["bcrypt", "argon2id"]).optional(),
|
||||
// Argon2id parameters (mirrors config/hashing.php).
|
||||
ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1),
|
||||
ARGON2_ITERATIONS: z.coerce.number().int().positive().default(4),
|
||||
ARGON2_MEMORY_SIZE: z.coerce.number().int().positive().default(65536),
|
||||
// Bcrypt cost factor (rounds).
|
||||
BCRYPT_ROUNDS: z.coerce.number().int().positive().default(12),
|
||||
// Filesystem dir the badge uploader writes <code>.gif into (the emulator's
|
||||
|
||||
@@ -21,6 +21,7 @@ const ROUTES: Array<[string, string]> = [
|
||||
["shop", "PERMS.SHOP_VIEW"],
|
||||
["transactions", "PERMS.SHOP_VIEW"],
|
||||
["vouchers", "PERMS.SHOP_VIEW"],
|
||||
["marketplace", "PERMS.SHOP_VIEW"],
|
||||
["vpn", "PERMS.SETTINGS_VIEW"],
|
||||
["ip", "PERMS.SETTINGS_VIEW"],
|
||||
["maintenance", "PERMS.SETTINGS_VIEW"],
|
||||
@@ -32,11 +33,26 @@ const ROUTES: Array<[string, string]> = [
|
||||
["badges", "PERMS.CATALOG_VIEW"],
|
||||
["teams", "PERMS.USERS_VIEW"],
|
||||
["applications", "PERMS.USERS_VIEW"],
|
||||
["guilds", "PERMS.USERS_VIEW"],
|
||||
["tickets", "PERMS.TICKETS_VIEW"],
|
||||
["help-tickets", "PERMS.TICKETS_VIEW"],
|
||||
["permissions", "PERMS.PERMISSIONS_MANAGE"],
|
||||
["housekeeping", "PERMS.SETTINGS_VIEW"],
|
||||
["logs", "PERMS.LOGS_VIEW"],
|
||||
["catalog", "PERMS.CATALOG_VIEW"],
|
||||
["rooms/edit/[id]", "PERMS.ROOMS_EDIT"],
|
||||
];
|
||||
|
||||
const MOD_ROUTES: Array<[string, string]> = [
|
||||
["", "requireMod"],
|
||||
["cfh", "PERMS.MOD_CFH_VIEW"],
|
||||
["actions", "PERMS.MOD_ACTIONS"],
|
||||
["bans", "PERMS.MOD_BANS_VIEW"],
|
||||
["tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||
["help-tickets", "PERMS.MOD_TICKETS_VIEW"],
|
||||
["team", "PERMS.MOD_TEAM_VIEW"],
|
||||
];
|
||||
|
||||
const ACTION_GATES: Array<[string, string]> = [
|
||||
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
|
||||
["admin-theme.ts", "PERMS.SETTINGS_EDIT"],
|
||||
@@ -54,6 +70,11 @@ const ACTION_GATES: Array<[string, string]> = [
|
||||
["admin-alerts.ts", "PERMS.NOTIFICATIONS_EDIT"],
|
||||
["commandocentrum.ts", "PERMS.RCON_EXECUTE"],
|
||||
["translations.ts", "PERMS.SETTINGS_EDIT"],
|
||||
["tickets.ts", "PERMS.TICKETS_EDIT"],
|
||||
["admin-help-tickets.ts", "PERMS.TICKETS_EDIT"],
|
||||
["permissions.ts", "PERMS.PERMISSIONS_MANAGE"],
|
||||
["admin-guilds.ts", "PERMS.USERS_EDIT"],
|
||||
["moderation.ts", "PERMS.MODERATION_EDIT"],
|
||||
];
|
||||
|
||||
describe("admin operations route contract", () => {
|
||||
@@ -63,6 +84,19 @@ describe("admin operations route contract", () => {
|
||||
expect(readFileSync(path, "utf8"), path).toContain(permission);
|
||||
});
|
||||
|
||||
it.each(MOD_ROUTES)("provides and guards /mod/%s", (route, permission) => {
|
||||
const path =
|
||||
route === "" ? "src/app/mod/page.tsx" : `src/app/mod/${route}/page.tsx`;
|
||||
expect(existsSync(path), path).toBe(true);
|
||||
const source = readFileSync(path, "utf8");
|
||||
const layout = readFileSync("src/app/mod/layout.tsx", "utf8");
|
||||
if (permission === "requireMod") {
|
||||
expect(layout).toContain("requireMod");
|
||||
} else {
|
||||
expect(source).toContain(permission);
|
||||
}
|
||||
});
|
||||
|
||||
it("guards radio section via layout", () => {
|
||||
const path = "src/app/admin/radio/layout.tsx";
|
||||
expect(existsSync(path), path).toBe(true);
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ export const { handlers, signOut, auth } = NextAuth({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Byte-compatible AtomCMS check (argon2id/bcrypt + md5->argon2id upgrade).
|
||||
// Byte-compatible AtomCMS check (bcrypt + md5->bcrypt upgrade).
|
||||
const res = await checkLogin(password, user.password, {
|
||||
convertPasswords: env.CONVERT_PASSWORDS,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { hash as bcryptHash } from "bcrypt";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockEnv = vi.hoisted(() => ({
|
||||
PASSWORD_HASH: undefined as string | undefined,
|
||||
ARGON2_PARALLELISM: 1,
|
||||
ARGON2_ITERATIONS: 4,
|
||||
ARGON2_MEMORY_SIZE: 65536,
|
||||
BCRYPT_ROUNDS: 12,
|
||||
}));
|
||||
|
||||
@@ -28,36 +23,20 @@ describe("md5Hex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashPassword (default driver: bcrypt)", () => {
|
||||
it("emits a PHP-style $2y$ bcrypt hash that fits varchar(64) and round-trips", async () => {
|
||||
mockEnv.PASSWORD_HASH = undefined;
|
||||
describe("hashPassword", () => {
|
||||
it("emits a bcrypt hash and round-trips", async () => {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$2y\$\d{2}\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyPassword", () => {
|
||||
it("verifies legacy bcrypt hashes ($2y$)", async () => {
|
||||
const h = await hashPassword("hunter2");
|
||||
expect(h).toMatch(/^\$2y\$/);
|
||||
expect(h.length).toBeLessThanOrEqual(60);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
|
||||
it("hashes with the AtomCMS params and round-trips", async () => {
|
||||
mockEnv.PASSWORD_HASH = "argon2id";
|
||||
mockEnv.ARGON2_MEMORY_SIZE = 1024;
|
||||
mockEnv.ARGON2_ITERATIONS = 1;
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$argon2id\$v=19\$m=1024,t=1,p=1\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bcrypt", () => {
|
||||
it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => {
|
||||
mockEnv.BCRYPT_ROUNDS = 4;
|
||||
const h = await bcryptHash("hunter2", 4);
|
||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||
const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
||||
expect(await verifyPassword("nope", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -71,13 +50,11 @@ describe("isMd5Of", () => {
|
||||
});
|
||||
|
||||
describe("checkLogin", () => {
|
||||
it("upgrades a legacy md5 hash to the configured hash when conversion is enabled", async () => {
|
||||
mockEnv.PASSWORD_HASH = undefined;
|
||||
it("upgrades a legacy md5 hash to bcrypt when conversion is enabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2y\$/);
|
||||
expect((res.upgradedHash as string).length).toBeLessThanOrEqual(60);
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -93,10 +70,9 @@ describe("checkLogin", () => {
|
||||
});
|
||||
|
||||
it("validates an existing modern hash with no upgrade", async () => {
|
||||
mockEnv.PASSWORD_HASH = undefined;
|
||||
const stored = await hashPassword("modern");
|
||||
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
-75
@@ -1,66 +1,26 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { compare as bcryptCompare, hash as bcryptHash } from "bcrypt";
|
||||
import { argon2id, argon2Verify, md5 } from "hash-wasm";
|
||||
import {
|
||||
bcrypt,
|
||||
bcryptVerify,
|
||||
md5,
|
||||
} from "hash-wasm";
|
||||
|
||||
import { env } from "@/env";
|
||||
|
||||
// AtomCMS hashing (config/hashing.php): argon2id with memory=65536 KiB, time=4,
|
||||
// threads=1; bcrypt rounds=12 as the legacy fallback. The game emulator
|
||||
// validates the SAME users.password hash, so these must match.
|
||||
function argon2Params() {
|
||||
return {
|
||||
parallelism: env.ARGON2_PARALLELISM,
|
||||
iterations: env.ARGON2_ITERATIONS,
|
||||
memorySize: env.ARGON2_MEMORY_SIZE,
|
||||
hashLength: 32,
|
||||
} as const;
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const h = await bcrypt({
|
||||
password,
|
||||
salt: randomBytes(16),
|
||||
costFactor: env.BCRYPT_ROUNDS,
|
||||
outputType: "encoded",
|
||||
});
|
||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
}
|
||||
|
||||
function bcryptRounds(): number {
|
||||
return env.BCRYPT_ROUNDS;
|
||||
}
|
||||
|
||||
// Which algorithm hashPassword() emits for NEW/upgraded passwords.
|
||||
// - "bcrypt" (DEFAULT): 60-char $2y$ hash. Fits varchar(255) users.password.
|
||||
// - "argon2id": ~97-char PHC hash. Opt in with PASSWORD_HASH=argon2id.
|
||||
// verifyPassword() always accepts BOTH, so logins keep working either way.
|
||||
function hashDriver(): "bcrypt" | "argon2id" {
|
||||
return env.PASSWORD_HASH === "argon2id" ? "argon2id" : "bcrypt";
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercase hex md5 of a UTF-8 string (matches PHP md5()).
|
||||
*
|
||||
* This uses hash-wasm's MD5 (not node:crypto) to match PHP's md5() output,
|
||||
* enabling verification of legacy AtomCMS password hashes during the on-login
|
||||
* upgrade path (isMd5Of → checkLogin). It is NOT used to hash new passwords
|
||||
* and does NOT affect credential security.
|
||||
*/
|
||||
export async function md5Hex(input: string): Promise<string> {
|
||||
return await md5(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a new password with the configured driver. Defaults to bcrypt ($2y$,
|
||||
* rounds=12) so the result fits a varchar(64) column; set PASSWORD_HASH=argon2id
|
||||
* for argon2id (requires a wider column). Both are verifiable by verifyPassword.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
if (hashDriver() === "argon2id") {
|
||||
return argon2id({
|
||||
password,
|
||||
salt: randomBytes(16),
|
||||
outputType: "encoded",
|
||||
...argon2Params(),
|
||||
});
|
||||
}
|
||||
// native bcrypt emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the
|
||||
// emulator and existing AtomCMS rows use.
|
||||
const h = await bcryptHash(password, bcryptRounds());
|
||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
}
|
||||
|
||||
/** True when `stored` is exactly the md5 of `password` (legacy AtomCMS accounts). */
|
||||
export async function isMd5Of(
|
||||
password: string,
|
||||
stored: string,
|
||||
@@ -71,27 +31,13 @@ export async function isMd5Of(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a password against a stored hash, auto-detecting the algorithm the way
|
||||
* Laravel's Hash::check does. Returns false for unknown/legacy formats (md5 is
|
||||
* handled by the conversion path in checkLogin, not here).
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
stored: string,
|
||||
): Promise<boolean> {
|
||||
if (stored.startsWith("$argon2")) {
|
||||
try {
|
||||
return await argon2Verify({ password, hash: stored });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (/^\$2[aby]\$/.test(stored)) {
|
||||
try {
|
||||
// PHP/AtomCMS store $2y$; native bcrypt only accepts $2a$/$2b$.
|
||||
const normalized = stored.replace(/^\$2y\$/, "$2a$");
|
||||
return await bcryptCompare(password, normalized);
|
||||
return await bcryptVerify({ password, hash: stored });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -101,15 +47,9 @@ export async function verifyPassword(
|
||||
|
||||
export interface LoginCheck {
|
||||
valid: boolean;
|
||||
/** Set when a legacy md5 hash was upgraded — persist it to users.password. */
|
||||
upgradedHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full AtomCMS credential check including the md5 -> argon2id on-login upgrade
|
||||
* (gated by `convertPasswords`, i.e. config('habbo.site.convert_passwords')).
|
||||
* Mirrors RedirectIfTwoFactorAuthenticatable::convertUserPassword + validate.
|
||||
*/
|
||||
export async function checkLogin(
|
||||
password: string,
|
||||
stored: string,
|
||||
@@ -119,4 +59,4 @@ export async function checkLogin(
|
||||
return { valid: true, upgradedHash: await hashPassword(password) };
|
||||
}
|
||||
return { valid: await verifyPassword(password, stored) };
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,10 @@ describe("production deploy workflow", () => {
|
||||
const stopAt = deployJob.indexOf("pm2 stop next");
|
||||
const migrateAt = deployJob.indexOf("pnpm db:migrate");
|
||||
const startLabelAt = deployJob.indexOf("Starting PM2");
|
||||
const startAt = deployJob.indexOf("pm2 reload next", startLabelAt);
|
||||
const startAt = deployJob.indexOf(
|
||||
"pm2 start pnpm --name next -- start",
|
||||
startLabelAt,
|
||||
);
|
||||
expect(buildAt).toBeGreaterThan(-1);
|
||||
expect(stopAt).toBeGreaterThan(buildAt);
|
||||
expect(migrateAt).toBeGreaterThan(stopAt);
|
||||
@@ -103,7 +106,9 @@ describe("production deploy workflow", () => {
|
||||
it("runs an HTTP health check before declaring deploy success", () => {
|
||||
expect(workflow).toContain("/api/health");
|
||||
expect(workflow).toContain('"database":true');
|
||||
const startAt = deployJob.indexOf("pm2 reload next");
|
||||
expect(deployJob).toContain("export PORT=");
|
||||
expect(deployJob).toContain("free_tcp_port");
|
||||
const startAt = deployJob.indexOf("pm2 start pnpm --name next -- start");
|
||||
const healthAt = deployJob.indexOf("/api/health");
|
||||
const successAt = deployJob.indexOf("--- Deployed successfully ---");
|
||||
expect(startAt).toBeGreaterThan(-1);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { Variants } from "framer-motion";
|
||||
import type { Variants } from "motion/react";
|
||||
|
||||
export const pageTransition: Variants = {
|
||||
initial: { opacity: 0, y: 8 },
|
||||
|
||||
+3
-71
@@ -1,76 +1,8 @@
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
/**
|
||||
* Server-side HTML sanitiser for user/staff-authored rich content before it is
|
||||
* injected via dangerouslySetInnerHTML — the AtomCMS HTMLPurifier equivalent.
|
||||
* Allows a safe formatting subset (no <script>/<style>/<iframe>, no on* event
|
||||
* handlers, no javascript: URLs); images/links are permitted with safe schemes.
|
||||
*/
|
||||
const OPTIONS: sanitizeHtml.IOptions = {
|
||||
allowedTags: [
|
||||
"a",
|
||||
"b",
|
||||
"i",
|
||||
"em",
|
||||
"strong",
|
||||
"u",
|
||||
"s",
|
||||
"p",
|
||||
"br",
|
||||
"hr",
|
||||
"span",
|
||||
"div",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"blockquote",
|
||||
"code",
|
||||
"pre",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"img",
|
||||
"figure",
|
||||
"figcaption",
|
||||
"table",
|
||||
"thead",
|
||||
"tbody",
|
||||
"tr",
|
||||
"th",
|
||||
"td",
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ["href", "title", "target", "rel"],
|
||||
img: ["src", "alt", "title", "width", "height"],
|
||||
"*": ["style", "class"],
|
||||
},
|
||||
allowedSchemes: ["http", "https", "mailto"],
|
||||
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
||||
// Drop any style declarations that aren't simple, safe properties.
|
||||
allowedStyles: {
|
||||
"*": {
|
||||
color: [/.*/],
|
||||
"background-color": [/.*/],
|
||||
"text-align": [/^left$|^right$|^center$|^justify$/],
|
||||
"font-weight": [/.*/],
|
||||
"font-style": [/.*/],
|
||||
"text-decoration": [/.*/],
|
||||
"font-size": [/.*/],
|
||||
margin: [/.*/],
|
||||
padding: [/.*/],
|
||||
},
|
||||
},
|
||||
transformTags: {
|
||||
a: sanitizeHtml.simpleTransform("a", {
|
||||
rel: "noopener noreferrer nofollow",
|
||||
}),
|
||||
},
|
||||
};
|
||||
const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);
|
||||
|
||||
export function sanitize(html: string | null | undefined): string {
|
||||
if (!html) return "";
|
||||
return sanitizeHtml(html, OPTIONS);
|
||||
return sanitizeHtml(html);
|
||||
}
|
||||
@@ -1,29 +1,12 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import nodemailer, { type Transporter } from "nodemailer";
|
||||
import { Resend } from "resend";
|
||||
import { env } from "@/env";
|
||||
import { logger } from "@/lib/logger";
|
||||
|
||||
let transporter: Transporter | null = null;
|
||||
let resend: Resend | null = null;
|
||||
|
||||
function getTransport(): Transporter | null {
|
||||
if (!env.SMTP_HOST) return null;
|
||||
if (!transporter) {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT ?? 587,
|
||||
secure: env.SMTP_SECURE,
|
||||
auth: env.SMTP_USER
|
||||
? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return transporter;
|
||||
}
|
||||
|
||||
function getResend(): Resend | null {
|
||||
if (!env.RESEND_API_KEY) return null;
|
||||
if (!resend) resend = new Resend(env.RESEND_API_KEY);
|
||||
@@ -90,7 +73,7 @@ async function writeToFile(
|
||||
}
|
||||
}
|
||||
|
||||
/** Send an HTML email. Tries Resend → SMTP → local sendmail → file fallback. Always returns true. */
|
||||
/** Send an HTML email. Tries Resend → local sendmail → file fallback. Always returns true. */
|
||||
export async function sendMail(
|
||||
to: string,
|
||||
subject: string,
|
||||
@@ -111,19 +94,6 @@ export async function sendMail(
|
||||
}
|
||||
}
|
||||
|
||||
const t = getTransport();
|
||||
if (t) {
|
||||
try {
|
||||
await t.sendMail({ from, to, subject, html });
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error("SMTP failed", {
|
||||
module: "email",
|
||||
error: (e as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ok = await sendViaSendmail(to, subject, html, from);
|
||||
if (ok) return true;
|
||||
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Затваряне на менюто",
|
||||
"drawBadge": "Начертайте значка",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} онлайн",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Табло за управление",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Цел",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Стаи",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Zavřít nabídku",
|
||||
"drawBadge": "Nakreslete odznak",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Cíl",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Pokoje",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Luk menuen",
|
||||
"drawBadge": "Tegn badge",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Mål",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Værelser",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Close menu",
|
||||
"drawBadge": "Abzeichen zeichnen",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -876,7 +877,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -2183,7 +2185,24 @@
|
||||
"auditTarget": "Ziel",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Räume",
|
||||
@@ -2412,7 +2431,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2674,7 +2695,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2769,6 +2792,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"radioRequests": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Κλείσιμο μενού",
|
||||
"drawBadge": "Σχεδιάστε το σήμα",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} στο διαδίκτυο",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Ταμπλό",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Στόχος",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Δωμάτια",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+12
-6
@@ -1492,8 +1492,10 @@
|
||||
"settingsSaved": "VPN settings saved successfully"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Tickets",
|
||||
"subtitle": "Support tickets from players",
|
||||
"title": "CMS support desk",
|
||||
"subtitle": "Internal CMS tickets (assignee, priority, staff workflow). Not the same as help-center tickets from /help.",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets",
|
||||
"noTickets": "No active tickets",
|
||||
"searchPlaceholder": "Search subject, user, category or ID…",
|
||||
"presetActive": "Active",
|
||||
@@ -1510,7 +1512,9 @@
|
||||
},
|
||||
"helpTickets": {
|
||||
"title": "Help center tickets",
|
||||
"subtitle": "Player tickets from /help/tickets",
|
||||
"subtitle": "Tickets players open at /help/tickets. Separate from the CMS support desk.",
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk",
|
||||
"noTickets": "No open help center tickets",
|
||||
"searchPlaceholder": "Search title, user or ID…",
|
||||
"presetOpen": "Open",
|
||||
@@ -2520,7 +2524,7 @@
|
||||
"events": "Events",
|
||||
"eventsSubtitle": "Hotel events and event types",
|
||||
"tickets": "Support desk",
|
||||
"ticketsSubtitle": "CMS tickets, help-center tickets, and reply templates",
|
||||
"ticketsSubtitle": "Two separate queues: CMS support desk and public help-center tickets",
|
||||
"staffAccess": "Ranks & permissions",
|
||||
"staffAccessSubtitle": "Staff ranks and live access. Archive is compare/export only.",
|
||||
"analytics": "Analytics",
|
||||
@@ -3004,7 +3008,8 @@
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"tickets": "CMS tickets",
|
||||
"helpTickets": "Help-center tickets",
|
||||
"team": "Team"
|
||||
},
|
||||
"stats": {
|
||||
@@ -3013,7 +3018,8 @@
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"openTickets": "Open CMS tickets",
|
||||
"openHelpTickets": "Open help tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload"
|
||||
}
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Close menu",
|
||||
"drawBadge": "Diseñar insignia",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} en línea",
|
||||
@@ -876,7 +877,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel",
|
||||
@@ -2183,7 +2185,24 @@
|
||||
"auditTarget": "Objetivo",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Salas",
|
||||
@@ -2412,7 +2431,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2674,7 +2695,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2769,6 +2792,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"radioRequests": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Close menu",
|
||||
"drawBadge": "Dessiner un badge",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} en ligne",
|
||||
@@ -876,7 +877,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord",
|
||||
@@ -2183,7 +2185,24 @@
|
||||
"auditTarget": "Cible",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Salons",
|
||||
@@ -2412,7 +2431,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2674,7 +2695,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2769,6 +2792,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"radioRequests": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Zatvori izbornik",
|
||||
"drawBadge": "Nacrtajte značku",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Nadzorna ploča",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "cilj",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Sobe",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Menü bezárása",
|
||||
"drawBadge": "Rajzolj jelvényt",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Irányítópult",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Cél",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Szobák",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+15
-9
@@ -1467,8 +1467,8 @@
|
||||
"settingsSaved": "Impostazioni VPN salvate con successo"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Ticket",
|
||||
"subtitle": "Ticket di supporto dai giocatori",
|
||||
"title": "Supporto CMS",
|
||||
"subtitle": "Ticket interni CMS (assegnatario, priorita, workflow staff). Non sono i ticket del help center /help.",
|
||||
"noTickets": "Nessun ticket attivo",
|
||||
"searchPlaceholder": "Cerca oggetto, utente, categoria o ID…",
|
||||
"presetActive": "Attivi",
|
||||
@@ -1481,11 +1481,13 @@
|
||||
"colPriority": "Priorità",
|
||||
"colAssigned": "Assegnato",
|
||||
"colDate": "Data",
|
||||
"meta": "di {user} · {count} msg · {category}"
|
||||
"meta": "di {user} · {count} msg · {category}",
|
||||
"queueHint": "Questa coda e il supporto CMS. I ticket del help center pubblici stanno nella tab Help-center tickets.",
|
||||
"queueOtherCta": "Apri help-center tickets"
|
||||
},
|
||||
"helpTickets": {
|
||||
"title": "Ticket centro assistenza",
|
||||
"subtitle": "Ticket dei giocatori da /help/tickets",
|
||||
"title": "Ticket help center",
|
||||
"subtitle": "Ticket aperti dai player su /help/tickets. Separati dal supporto CMS.",
|
||||
"noTickets": "Nessun ticket aperto nel centro assistenza",
|
||||
"searchPlaceholder": "Cerca titolo, utente o ID…",
|
||||
"presetOpen": "Aperti",
|
||||
@@ -1516,7 +1518,9 @@
|
||||
"replied": "Risposta inviata.",
|
||||
"closed": "Ticket chiuso.",
|
||||
"reopened": "Ticket riaperto."
|
||||
}
|
||||
},
|
||||
"queueHint": "Questa coda e il help center pubblico. I ticket del supporto CMS stanno nella tab Support desk.",
|
||||
"queueOtherCta": "Apri supporto CMS"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2791,7 +2795,7 @@
|
||||
"events": "Eventi",
|
||||
"eventsSubtitle": "Eventi hotel e tipi evento",
|
||||
"tickets": "Assistenza",
|
||||
"ticketsSubtitle": "Ticket CMS, ticket centro assistenza e template risposte",
|
||||
"ticketsSubtitle": "Due code separate: supporto CMS e ticket help center pubblici",
|
||||
"staffAccess": "Gradi e permessi",
|
||||
"staffAccessSubtitle": "Gradi staff e accesso live. L'archivio e solo confronto/export.",
|
||||
"analytics": "Analitiche",
|
||||
@@ -3011,7 +3015,8 @@
|
||||
"bans": "Ban",
|
||||
"fullAdmin": "Admin completo",
|
||||
"backToSite": "Torna al sito",
|
||||
"tickets": "Ticket",
|
||||
"tickets": "Ticket CMS",
|
||||
"helpTickets": "Ticket help center",
|
||||
"team": "Team"
|
||||
},
|
||||
"stats": {
|
||||
@@ -3020,7 +3025,8 @@
|
||||
"modsOnline": "Mod online",
|
||||
"quickActions": "Azioni rapide",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Ticket aperti",
|
||||
"openTickets": "Ticket CMS aperti",
|
||||
"openHelpTickets": "Help ticket aperti",
|
||||
"team": "Team mod",
|
||||
"teamHint": "Staff online e carico"
|
||||
}
|
||||
|
||||
+15
-9
@@ -2842,7 +2842,7 @@
|
||||
"events": "Evenementen",
|
||||
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
|
||||
"tickets": "Supportbalie",
|
||||
"ticketsSubtitle": "CMS-tickets, helpcentrum-tickets en antwoordtemplates",
|
||||
"ticketsSubtitle": "Twee aparte wachtrijen: CMS support desk en publieke helpcenter-tickets",
|
||||
"staffAccess": "Rangen & rechten",
|
||||
"staffAccessSubtitle": "Staffrangen en live toegang. Archief is alleen vergelijken/exporteren.",
|
||||
"analytics": "Analytics",
|
||||
@@ -2851,8 +2851,8 @@
|
||||
"devopsSubtitle": "Servicegezondheid en emulatorfouten"
|
||||
},
|
||||
"tickets": {
|
||||
"title": "Tickets",
|
||||
"subtitle": "Ondersteuningstickets van spelers",
|
||||
"title": "CMS support desk",
|
||||
"subtitle": "Interne CMS-tickets (assignee, prioriteit, staff-workflow). Niet hetzelfde als helpcenter-tickets van /help.",
|
||||
"noTickets": "Geen actieve tickets",
|
||||
"searchPlaceholder": "Zoek op onderwerp, gebruiker, categorie of ID…",
|
||||
"presetActive": "Actief",
|
||||
@@ -2865,11 +2865,13 @@
|
||||
"colPriority": "Prioriteit",
|
||||
"colAssigned": "Toegewezen",
|
||||
"colDate": "Datum",
|
||||
"meta": "door {user} · {count} berichten · {category}"
|
||||
"meta": "door {user} · {count} berichten · {category}",
|
||||
"queueHint": "Deze wachtrij is de CMS support desk. Speler-helpcenter tickets staan onder Help-center tickets.",
|
||||
"queueOtherCta": "Open helpcenter-tickets"
|
||||
},
|
||||
"helpTickets": {
|
||||
"title": "Helpcentrum-tickets",
|
||||
"subtitle": "Spelertickets van /help/tickets",
|
||||
"title": "Helpcenter-tickets",
|
||||
"subtitle": "Tickets die spelers openen op /help/tickets. Apart van de CMS support desk.",
|
||||
"noTickets": "Geen open helpcentrum-tickets",
|
||||
"searchPlaceholder": "Zoek op titel, gebruiker of ID…",
|
||||
"presetOpen": "Open",
|
||||
@@ -2900,7 +2902,9 @@
|
||||
"replied": "Antwoord verzonden.",
|
||||
"closed": "Ticket gesloten.",
|
||||
"reopened": "Ticket heropend."
|
||||
}
|
||||
},
|
||||
"queueHint": "Deze wachtrij is het publieke helpcenter. CMS support desk tickets staan onder Support desk.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -3009,7 +3013,8 @@
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Volledige admin",
|
||||
"backToSite": "Terug naar site",
|
||||
"tickets": "Tickets",
|
||||
"tickets": "CMS-tickets",
|
||||
"helpTickets": "Helpcenter-tickets",
|
||||
"team": "Team"
|
||||
},
|
||||
"stats": {
|
||||
@@ -3018,7 +3023,8 @@
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Snelle acties",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"openTickets": "Open CMS-tickets",
|
||||
"openHelpTickets": "Open help-tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & werklast"
|
||||
}
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Lukk menyen",
|
||||
"drawBadge": "Tegn merke",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashbord",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Mål",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Rom",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Zamknij menu",
|
||||
"drawBadge": "Narysuj odznakę",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Pulpit nawigacyjny",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Cel",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Pokoje",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Fechar menu",
|
||||
"drawBadge": "Desenhar distintivo",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} on-line",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Painel",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Alvo",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Quartos",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Închide meniul",
|
||||
"drawBadge": "Trage insigna",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tabloul de bord",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "țintă",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Camerele",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Закрыть меню",
|
||||
"drawBadge": "Нарисуйте значок",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} онлайн",
|
||||
@@ -904,7 +905,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
@@ -2211,7 +2213,24 @@
|
||||
"auditTarget": "Target",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Rooms",
|
||||
@@ -2440,7 +2459,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2702,7 +2723,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2797,6 +2820,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2942,6 +2990,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Zatvoriť ponuku",
|
||||
"drawBadge": "Nakreslite odznak",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Cieľ",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Izby",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Затвори мени",
|
||||
"drawBadge": "Нацртај значку",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} на мрежи",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Дасхбоард",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Таргет",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Собе",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Stäng menyn",
|
||||
"drawBadge": "Rita märke",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} online",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Instrumentbräda",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Mål",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Rum",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Menüyü kapat",
|
||||
"drawBadge": "Rozet çiz",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} çevrimiçi",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Kontrol Paneli",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Hedef",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Odalar",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
+82
-5
@@ -23,7 +23,8 @@
|
||||
"closeMenu": "Закрити меню",
|
||||
"drawBadge": "Намалюйте значок",
|
||||
"events": "Events",
|
||||
"polls": "Polls"
|
||||
"polls": "Polls",
|
||||
"mod": "Mod"
|
||||
},
|
||||
"header": {
|
||||
"online": "{count} {hotel} онлайн",
|
||||
@@ -906,7 +907,8 @@
|
||||
"chatLog": "Chat log",
|
||||
"commandLog": "Command log",
|
||||
"tradeLog": "Trade log",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"guilds": "Guilds"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Приладова панель",
|
||||
@@ -2213,7 +2215,24 @@
|
||||
"auditTarget": "Цільова",
|
||||
"selectAllMatching": "Select all {count} matching",
|
||||
"save": "Save",
|
||||
"importTitle": "Import permissions"
|
||||
"importTitle": "Import permissions",
|
||||
"legacyBannerTitle": "Use Permissions to change who can do what",
|
||||
"legacyBannerBody": "This archive does not control the site. Open Permissions to grant or revoke access for ranks.",
|
||||
"legacyBannerCta": "Open Permissions",
|
||||
"writesRetired": "Editing here is disabled on purpose. Change access only in Permissions.",
|
||||
"tabCompare": "ACL compare",
|
||||
"tabExportOnly": "Export archive",
|
||||
"compareHint": "Compares legacy housekeeping min-rank rows to live ACL rank_* role grants (lowest rank that has the slug).",
|
||||
"compareAligned": "{count} aligned",
|
||||
"compareMissingAcl": "{count} missing in ACL",
|
||||
"compareMismatch": "{count} rank mismatch",
|
||||
"colAclPresent": "In ACL",
|
||||
"colAclMinRank": "ACL min rank",
|
||||
"colStatus": "Status",
|
||||
"statusAligned": "Aligned",
|
||||
"statusMissingAcl": "Missing in ACL",
|
||||
"statusMismatch": "Rank differs",
|
||||
"exportOnlyHint": "Writes are disabled. You can still export the legacy table for archival."
|
||||
},
|
||||
"rooms": {
|
||||
"title": "Кімнати",
|
||||
@@ -2442,7 +2461,9 @@
|
||||
"colPriority": "Priority",
|
||||
"colAssigned": "Assigned",
|
||||
"colDate": "Date",
|
||||
"meta": "by {user} · {count} msg · {category}"
|
||||
"meta": "by {user} · {count} msg · {category}",
|
||||
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
|
||||
"queueOtherCta": "Open help-center tickets"
|
||||
},
|
||||
"cfh": {
|
||||
"title": "Call for Help",
|
||||
@@ -2704,7 +2725,9 @@
|
||||
"replied": "Reply sent.",
|
||||
"closed": "Ticket closed.",
|
||||
"reopened": "Ticket reopened."
|
||||
}
|
||||
},
|
||||
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
|
||||
"queueOtherCta": "Open CMS support desk"
|
||||
},
|
||||
"marketplace": {
|
||||
"title": "Marketplace",
|
||||
@@ -2799,6 +2822,31 @@
|
||||
"less": "Less",
|
||||
"more": "More"
|
||||
}
|
||||
},
|
||||
"guilds": {
|
||||
"title": "Guilds",
|
||||
"subtitle": "Hotel groups and membership",
|
||||
"totalGuilds": "Guilds",
|
||||
"searchPlaceholder": "Search by name, description or ID…",
|
||||
"search": "Search",
|
||||
"empty": "No guilds found.",
|
||||
"loadError": "Could not load guilds (database unavailable).",
|
||||
"colId": "ID",
|
||||
"colBadge": "Badge",
|
||||
"colName": "Name",
|
||||
"colOwner": "Owner",
|
||||
"colMembers": "Members",
|
||||
"colCreated": "Created",
|
||||
"colThreads": "Forum threads",
|
||||
"colRoom": "Room",
|
||||
"colUser": "User",
|
||||
"colLevel": "Level",
|
||||
"colJoined": "Joined",
|
||||
"members": "Members",
|
||||
"noMembers": "No members.",
|
||||
"back": "Back to guilds",
|
||||
"viewPublic": "Open public page",
|
||||
"disband": "Disband guild"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -2944,6 +2992,35 @@
|
||||
"voteCount": "{count} ({pct}%)",
|
||||
"totalVotes": "{count} total votes",
|
||||
"textResponses": "{count} free-text responses"
|
||||
},
|
||||
"mod": {
|
||||
"title": "Moderation panel",
|
||||
"badge": "Mod",
|
||||
"overviewTitle": "Moderator overview",
|
||||
"overviewSubtitle": "CFH, quick actions and bans without full admin access.",
|
||||
"shortcutsTitle": "Shortcuts",
|
||||
"nav": {
|
||||
"overview": "Overview",
|
||||
"cfh": "Call for Help",
|
||||
"actions": "Quick actions",
|
||||
"bans": "Bans",
|
||||
"fullAdmin": "Full admin",
|
||||
"backToSite": "Back to site",
|
||||
"tickets": "Tickets",
|
||||
"team": "Team",
|
||||
"helpTickets": "Help-center tickets"
|
||||
},
|
||||
"stats": {
|
||||
"openCfh": "Open CFH",
|
||||
"bansToday": "Bans today",
|
||||
"modsOnline": "Mods online",
|
||||
"quickActions": "Quick actions",
|
||||
"quickActionsHint": "Kick, mute, alert",
|
||||
"openTickets": "Open tickets",
|
||||
"team": "Mod team",
|
||||
"teamHint": "Online staff & workload",
|
||||
"openHelpTickets": "Open help tickets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
Reference in new issue
Block a user