2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00
2026-07-05 16:54:59 -07:00

Reforger Panel

A private, purpose-built control panel for one community Arma Reforger training/recruiting server. It sits on top of Pterodactyl — Pterodactyl (and Wings) keep running the game container, files, backups, and allocations; this panel is the curated management experience for the owner and trusted crew admins.

Not included by design: billing, public sign-up, multi-tenancy, arbitrary file management, raw console access, or anything that replaces Pterodactyl.

Architecture

Browser (React SPA)
  │  same-origin /api only — no upstream credentials ever reach the browser
  ▼
Panel API (Express + TypeScript)
  ├─ Discord OAuth → local users, sessions (Postgres), roles
  ├─ PostgreSQL (Drizzle ORM)
  ├─ Workshop client → https://api.reforgermods.net (backend-only)
  ├─ GameServerProvider abstraction
  │     ├─ PterodactylProvider (Client API: status, resources, power, read-only files)
  │     └─ MockGameServerProvider (full local dev without credentials)
  └─ Log ingestion worker: Pterodactyl log download → parser → players/sessions/events

Monorepo layout:

apps/api        Express API, Drizzle schema/migrations, ingestion worker, tests
apps/web        Vite + React + Tailwind dashboard
packages/shared Roles/capabilities, DTO types, Reforger config model

Quick start (mock mode, no Pterodactyl or Discord needed)

cp .env.example .env            # defaults are fine for local dev
# set DEV_AUTH_BYPASS=true in .env to log in without Discord

docker compose up -d            # Postgres on 127.0.0.1:5433
npm install
npm run db:migrate
npm run db:seed                 # creates the server row (real data is imported from the server)
npm run dev                     # API on :3001, web on :5173

Open http://localhost:5173 and use Local development login (requires DEV_AUTH_BYPASS=true; the endpoint refuses to exist in production). Mock mode serves a generated console.log, so within ~20 s the dashboard shows players, sessions, and events produced by the real ingestion pipeline.

Useful scripts: npm run lint, npm run typecheck, npm test, npm run build, npm run format, npm run db:generate (new migration after schema changes).

Environment variables

See .env.example for the full annotated list. Highlights:

Variable Purpose
DATABASE_URL, SESSION_SECRET Postgres + cookie/state signing (32+ chars)
DISCORD_CLIENT_ID/SECRET, DISCORD_REDIRECT_URI Discord OAuth app
OWNER_DISCORD_ID This Discord account is auto-assigned owner at login
DEV_AUTH_BYPASS Local-only fake owner login; rejected when NODE_ENV=production
REFORGER_WORKSHOP_API_BASE_URL Workshop metadata API (backend-only)
PTERODACTYL_BASE_URL, PTERODACTYL_CLIENT_API_KEY, PTERODACTYL_SERVER_ID Client API (not Application API)
USE_MOCK_PTERODACTYL true = run entirely against the in-process mock
REFORGER_CONFIG_PATH, REFORGER_CONFIG_SYNC_INTERVAL_SECONDS Where the server's config.json lives; imported read-only at startup + interval
REFORGER_LOG_DIRECTORY, REFORGER_LOG_FILE_PATTERN Recommended: directory listed each sync; newest logs_* subfolder is followed
REFORGER_ADMIN_LOG_PATH Optional: pins one exact log file, overriding directory discovery
REFORGER_LOG_POLL_INTERVAL_SECONDS / _MAX_DOWNLOAD_BYTES / _STALE_AFTER_SECONDS Ingestion pacing, download cap, staleness threshold

Environment is validated with zod at startup; the process refuses to boot with missing/contradictory settings (e.g. real mode without Pterodactyl credentials).

Users, roles, and enforcement

Roles live in the panel database (not Discord roles). New users default to viewer; the account matching OWNER_DISCORD_ID becomes owner automatically. The owner manages roles under Settings.

Capability owner server_admin mission_lead viewer
View dashboard/server/players/activity
Start / stop
Restart
Operational health diagnostics
Manual log sync
User/role management, settings

Enforcement is backend middleware (requireAuth + requireCapability); the frontend only hides buttons. Sessions are 7-day HTTP-only cookies (SameSite=Lax, Secure in production), stored in Postgres as SHA-256 hashes and revocable server-side. State-changing requests additionally require a custom X-CSRF-Protection header and pass an Origin allowlist; OAuth uses a signed state cookie. Auth, power, and sync endpoints are rate limited.

Log ingestion

Flow: panel backend → Pterodactyl Client API (signed download URL, streamed with a byte cap) → parser → Postgres. Wings is never touched directly and the browser never downloads logs.

  • Scheduler — one poll loop per server (default 20 s), per-server lock so syncs never overlap, exponential backoff (up to 8×) after consecutive failures, graceful shutdown that waits for in-flight syncs. Starts only when a provider and log path are configured. Owner can trigger POST /api/servers/:slug/logs/sync manually.
  • Cursoring — a log_cursors row per (server, path) stores byte offset, file fingerprint (hash of the first line when visible), hash of the last processed line, and any partial trailing line.
    • First sync: only a bounded tail (512 KiB) is imported, never full history.
    • Append: only bytes after the cursor are parsed; a stored partial line is prepended.
    • Rotation/truncation/replacement: detected via size decrease, fingerprint change, or a continuity mismatch at the cut point → cursor resets and a bounded tail of the new file is processed.
    • Partial trailing lines are never parsed; they wait for the next sync.
    • Large files: downloads are capped (REFORGER_LOG_MAX_DOWNLOAD_BYTES); if the file grew past the window, the gap is noted and a bounded tail is processed. Retrieval is isolated in pterodactyl-log-source.ts so range/tail requests can be added without touching parsing.
  • Deduplication — events carry a unique (server_id, source_log_path, sha256(raw line)) key enforced by a Postgres unique index, so rotation boundaries and cursor resets cannot double-import.
  • Sessions — connect opens a session; disconnect closes it with duration; a reconnect without a disconnect closes the stale session (missed_disconnect); a fresh server start closes all open sessions (server_restart) and emits server_restart_detected.

Supported log events and known limitations

Recognized today (patterns centralized in apps/api/src/modules/reforger-logs/parser/patterns.ts, verified against real server logs):

  • Player #N Name (ip:port) connected (BattlEye wrapper) → player_connected
  • Player #N Name disconnectedplayer_disconnected
  • Player #N Name - BE GUID: … → merged into the player as a stable identity
  • Authenticated player: … identityId=<uuid> name=<name> (BACKEND channel) → engine-level identity, available even without BattlEye
  • Game successfully created / Server is ready to accept connectionsserver_started

When both identity lines appear for a player, the first one wins and the other is ignored, so a player is never split into duplicates.

Limitations to keep in mind:

  • Patterns can change between game versions. They were validated against a live 2026 server log, but Bohemia can change the format; adjust patterns.ts (each pattern has a fixture-backed test).
  • Player identity: when logs provide no GUID, players are matched by display name only — two people with the same name would merge, and renames create a new player record. The GUID line, when present, upgrades matching to a stable ID.
  • Timestamps in Reforger logs are time-of-day only; the date comes from the Log started header or falls back to the sync date, with midnight-rollover and future-timestamp guards. Cross-midnight logs without a header can be off by a day in pathological cases.
  • Player data is log-polled, not real-time — the UI always shows "last synchronized" and flags staleness rather than pretending to be live.

Finding the log location in Pterodactyl

Open your server in Pterodactyl → Files. Reforger writes a new dated folder per boot (e.g. /profile/logs/logs_2026-07-04_12-54-04/console.log). Set REFORGER_LOG_DIRECTORY to the parent (e.g. /profile/logs) — the panel lists it on every sync and follows the newest logs_* folder automatically, so restarts need no reconfiguration. REFORGER_ADMIN_LOG_PATH exists to pin one exact file and overrides discovery.

Configuration import

The panel downloads the server's real config.json (default /config.json, override with REFORGER_CONFIG_PATH) at startup, every REFORGER_CONFIG_SYNC_INTERVAL_SECONDS, and on demand via Configurations → Sync from server (owner/server admin). Each change creates a new ConfigRevision, and the server's displayed name and max players always come from the imported config — nothing is hand-seeded. Credentials in config.json (admin password, RCON password) are never copied into the panel's model.

Deploying privately (you + friends)

The API serves the built web app itself in production, so the whole panel is one container plus Postgres:

cp .env.example .env    # set Discord creds, OWNER_DISCORD_ID, Pterodactyl vars,
                        # a fresh SESSION_SECRET (openssl rand -base64 32),
                        # USE_MOCK_PTERODACTYL=false, DEV_AUTH_BYPASS=false
docker compose -f docker-compose.prod.yml up -d --build

Then put HTTPS in front of port 3001 — any of:

  • Tailscale (easiest for a private group): tailscale serve https / http://localhost:3001, share the tailnet with your friends.
  • Caddy: reverse_proxy localhost:3001 with a domain (automatic HTTPS).
  • nginx + certbot if you already run it.

Finally set WEB_ORIGIN and DISCORD_REDIRECT_URI in .env to the public URL (e.g. https://panel.example.com and https://panel.example.com/api/auth/discord/callback), register that redirect URI in your Discord application, and restart the stack. Production mode enforces Secure cookies (HTTPS required), refuses DEV_AUTH_BYPASS, and requires Discord credentials at boot.

Access model for a private group: anyone with the URL can log in with Discord but lands as a viewer with read-only access; hand out invite links (Settings → Invites) to grant server admin / mission lead roles, and manage roles under Settings → Users.

Connecting a real Pterodactyl server safely

  1. In Pterodactyl, log in as a user that has access to only this game server (create a dedicated sub-user if needed).
  2. Account Settings → API Credentials → create a Client API key. This scopes the panel to that user's servers — do not use an admin/Application API key.
  3. Set PTERODACTYL_BASE_URL, PTERODACTYL_CLIENT_API_KEY, PTERODACTYL_SERVER_ID (the short identifier from the server URL), USE_MOCK_PTERODACTYL=false, and REFORGER_ADMIN_LOG_PATH.
  4. Update the seeded server row if needed (the seed stores PTERODACTYL_SERVER_ID when present).
  5. Restart the API and check Settings → Integrations and the dashboard's Operational health card.

The API key stays server-side; requests have 1030 s timeouts, size-capped downloads, and errors are sanitized (no key, no host, no stack traces) before storage or display.

API surface

GET  /api/auth/me           POST /api/auth/logout
GET  /api/auth/discord      GET  /api/auth/discord/callback
POST /api/auth/dev-login    (dev only)

GET  /api/servers                         GET /api/servers/:slug
GET  /api/servers/:slug/resources         GET /api/servers/:slug/players
GET  /api/servers/:slug/players/known     GET /api/servers/:slug/activity
GET  /api/servers/:slug/configuration     GET /api/servers/:slug/mod-packs
POST /api/servers/:slug/power/{start,stop,restart}
POST /api/servers/:slug/logs/sync         GET /api/servers/:slug/logs/health

GET  /api/workshop/health   GET /api/workshop/search?q=&page=&sort=
GET  /api/workshop/mods/:id

GET  /api/users             PATCH /api/users/:id/role      (owner only)

Errors are structured: { "error": { "code", "message", "requestId" } }.

Implemented vs scaffolded

Implemented: Discord OAuth + sessions + role enforcement, dashboard + server pages, provider abstraction with mock and real Pterodactyl Client API, power controls with per-role limits (audited to the activity feed, simulated in mock mode), Workshop health/search/detail proxy, read-only config preview + revision history, full log ingestion pipeline (scheduler, cursoring, rotation, dedupe, sessions), operational health card, owner user/role management, 63 tests.

Scaffolded / later phases: mod-pack editing and deployment ("Add to pack" is intentionally disabled), config generation/writing to the server (no file writes through Pterodactyl yet), config presets for mission leads, Discord-role sync, multi-server support (schema is ready; UI assumes one), historical playtime analytics.

Recommended next steps: (1) capture real console.log samples from your server and harden the parser fixtures; (2) mod-pack builder writing ModPackRevisions from Workshop search; (3) config generation producing a real config.json diff/preview from ConfigRevision, then a guarded deploy (file write + restart) for owner/server admin; (4) preset selection for mission leads; (5) session-history charts from player_sessions.

S
Description
Connects to a pterodactyl API to have a custom dashboard to better manage Arma Reforger Servers, with mod manager.
Readme
547 KiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 98.7%
CSS 1%
Dockerfile 0.2%