Development
Code layout, test layers, internationalisation and the release chain.
Getting started#
cp .env.example .env.local # at minimum ADMIN_PASSWORD and SESSION_SECRET
npm install
npm run dev # http://localhost:3000npm run lint # eslint
npm run typecheck # next typegen && tsc --noEmit
npm test # vitest
npm run build.env must set RCON_PASSWORD_FILE (for instance
./data/config/rconpw) or RCON_PASSWORD, otherwise the
default path /factorio-config/rconpw does not exist on the host. To
open the panel from another machine, set NEXT_DEV_ORIGINS, or hot
reload is blocked.
Architecture#
src/
├── app/ pages + API routes (pages live under `[locale]/`)
├── components/ interface (console, actions, audit, metrics, dialog)
├── hooks/ server status, command execution, translations
├── i18n/ routing, dictionary loading, locale-aware navigation
├── lib/ shared client/server code (types, permissions, Lua templates)
└── server/ server-only code
├── actions/ business definitions, operator catalogue, execution
├── audit/ SQLite log
├── auth/ accounts, sessions, rate limiters
├── config/ validated environment variables (zod)
├── http/ route wrapper (session, permission, origin, logs)
├── metrics/ Docker client, collector, time series
└── rcon/ service, queue, parsing, status cache
messages/ dictionaries (en.json is the reference)
docs/ this siteTwo rules shape the server code:
- No interface text under
src/server/. The API only returns identifiers (ban,moderation) and error codes (validation_player), which the interface translates. The single documented exception is custom commands, which carry their own text since a catalogue written outside the repository cannot feed the dictionaries. - Singletons live on
globalThisto survive hot reload. The corollary: two module graphs can share an object without sharing the class that created it — which is why errors carry a mark taken from the global symbol registry instead of being recognised byinstanceof.
Tests#
| Path | Covers |
|---|---|
tests/<domain>/ | Pure logic: RCON queue and lifecycle, Lua escaping, catalogue validation, sessions, rate limiters, metrics, dictionaries |
tests/api/ | The route handlers, called with a real Request: authentication, roles, validation, error codes, audit trail |
tests/security/ | What must not regress: origin checking on every mutating route, X-Forwarded-For ignored without TRUST_PROXY, cookie flags, CSP nonce |
tests/proxy/ | The session guard: no combination of cookie state may produce a redirect cycle |
Internationalisation#
English (default, unprefixed) and French (/fr). The language is
inferred from Accept-Language on the first visit, switchable from
the status bar, and remembered in a cookie.
To add a language:
- copy
messages/en.jsontomessages/<code>.jsonand translate the values; - add the code to
localesinsrc/i18n/routing.ts; - add its name under
localeSwitcherin every dictionary; npm test— the suite checks that all dictionaries expose exactly the same keys, that no catalogue action is missing a label, and that every message compiles as ICU.
Deliberately left untranslated: raw Factorio output, RCON commands and server logs — they are aimed at the operator.
Releases#
semantic-release reads commit messages following the conventional
commits convention: fix: cuts a patch, feat: a
minor, chore: releases nothing. The released version is tagged,
CHANGELOG.md is updated, and the Docker images are pushed.
Dependabot#
.github/dependabot.yml drives dependency updates. Dependabot is
native to GitHub: there is no app to install, the file is enough. Three
ecosystems are tracked — npm, docker (base
images) and github-actions.
The choice that matters is the commit prefix, because semantic-release reads it:
| Source | Commit produced | Effect |
|---|---|---|
dependencies | fix(deps): … | Cuts a version and rebuilds the image. That is deliberate: a security fix has to reach deployments. |
devDependencies | chore(deps-dev): … | No release. |
| Docker, GitHub Actions | chore(deps): … | No release. |
Updates are grouped to avoid one PR per package: next and
react together — a major of one forces the major of the other
— then eslint, vitest, tailwindcss,
and the rest of the tooling in a single PR.
Dockerfiles. Left to align by hand, in the
same PR: engines in package.json and
node-version in .github/workflows/ci.yml.
This site#
The documentation is static HTML in docs/: no build step, no
dependency, no external resource. Editing a page means editing an
.html file; the styling lives in
docs/assets/style.css. Crawlers are pointed at
docs/robots.txt and docs/sitemap.xml (live under
the Pages prefix). Submit the sitemap in Google Search Console: a project
site on github.io does not own the host-root
/robots.txt.
It is published by GitHub Pages. Two ways to enable it:
- Actions — Settings → Pages → Source: GitHub
Actions. The
.github/workflows/pages.ymlworkflow publishesdocs/on every push tomain; - Branch — Settings → Pages → Source: Deploy from a
branch, branch
main, folder/docs. The workflow then becomes unnecessary and can be deleted.