- TypeScript 50%
- Vue 37.2%
- JavaScript 6.7%
- CSS 5.4%
- Dockerfile 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The design's register is a 14px root with 11-13px component text. That reads on a 1080p panel and is too small on a 27-34" 1440p one, where it needed browser zoom at 140% to be comfortable. zoom is the right tool rather than simply enlarging fonts. The interface carries 226 hardcoded pixel values in components and 95 more in the CSS — control heights, badge heights, minimum column widths. Growing type alone would overflow every fixed-height box it sits in. zoom scales type, spacing, borders and controls together, which is exactly what the browser does at 140% and why that looked right. Body text goes 13px -> 18.2px, labels 12 -> 16.8, h1 24 -> 33.6. One consequence had to be handled: zoom multiplies viewport units. 100vh resolves against the unzoomed viewport and is then scaled, so a plain h-screen sidebar rendered 2016px against a 1440px screen. Both viewport units in app.vue are now divided by --ui-scale, which lands the sidebar back at exactly one viewport. Verified in a browser at 2560x1440 and 3440x1440 across the character planner, thrall planner and the weapons table: sidebar fits, no horizontal overflow, zero console errors. Retune from the one number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| app | ||
| docs | ||
| public | ||
| scripts | ||
| server | ||
| test | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| nuxt.config.ts | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Forge of Exiles
A character planner, thrall planner and reference database for Conan Exiles (Enhanced) — built on stats read directly out of the game's own DataTables rather than community wiki transcription.
Not affiliated with Funcom.
| Surface | Route | What it does |
|---|---|---|
| Character planner | /planner/character |
60-point attribute budget, gear, armor→damage-reduction curve, A/B build compare |
| Thrall planner | /planner/thrall |
Two candidates side by side at a shared level; worst/expected/best attribute projections |
| Weapons | /weapons |
1,270 weapons, filterable by the effects they actually apply (bleed, sunder, cripple…) |
| Armor | /armor |
1,763 pieces |
| Thralls | /thralls |
4,602 capturable |
| Recipes | /recipes |
6,311 crafting chains |
| Builds | /builds |
Saved character and thrall builds — sign in with Steam to save, rename, and publish |
Quick start
npm install
npm run dev # http://localhost:3000
That's it — the game data is committed to the repo, so a fresh clone runs without the extraction toolchain. You do not need Conan Exiles installed to work on this.
npm run build # production build
npm run preview # serve the build locally
npm test # run the test suite
Where the data comes from
app/data/*.json is generated, not hand-written. It is produced by
scripts/build-data.mjs, which reads raw CUE4Parse
DataTable dumps of the shipped game files and emits trimmed, typed JSON.
node scripts/build-data.mjs [extractDir] # default: ../conan-extract/out
The extract directory is not part of this repo and must be produced separately. If you don't have it, the script exits with a clear message — that is expected, and it is not a broken checkout.
Game data is identical for every user and never changes at runtime, so it ships
in the bundle rather than a database. app/data/meta.json records the build
timestamp, source, row counts and facet histograms.
Do not hand-edit anything in app/data/ — it will be silently overwritten
the next time the pipeline runs. Corrections belong in scripts/build-data.mjs.
app/data-curated/ is the opposite: hand-authored TypeScript (player and thrall
perks) that has no upstream source in the game files. Edit those directly.
Architecture
app/
pages/ routes (Nuxt file-based routing)
components/
ds/ design-system primitives — Button, Input, Select, Tabs…
planner/ character + thrall planner UI
reference/ the dense data tables
ui/ app-level shell pieces
composables/ stateful logic — useBuild, useGear, useSettings, useThrallSide
utils/ ALL arithmetic — characterMath, thrallMath, gearMath,
recipeChain, effects, palette
data/ GENERATED game data (see above)
data-curated/ hand-authored perk tables
assets/css/ main.css — the design token layer
server/
api/ Steam auth + saved-builds routes — thin, call into server/utils/
routes/b/[id] short-link redirect for a published build
utils/ steam.ts, db.ts, ids.ts, session.ts, builds.ts, shortlink.ts —
plain functions, no Nitro auto-imports, unit-testable without
a running server. h3 helpers are imported explicitly and
runtime config arrives as an argument: the handler reads
useRuntimeConfig() and passes the secret and database path in
scripts/
build-data.mjs the data pipeline
test/
utils/ specs for app/utils/
composables/ specs for app/composables/ (Nuxt environment)
server/ specs for server/utils/ (node environment, no server needed)
docs/
design-handoff.md design intent + the Corrosion skin rationale
All maths lives in app/utils/. Components and composables call into it;
they don't re-derive. If you find yourself computing a stat inside a .vue
file, it belongs in a util instead — derivePlayer, damageReduction,
projectAttr, craftedArmor, craftedWeight, craftedDurability,
seedChances, weightClassOf already exist.
Design system
The UI is the Corrosion Control Panel design system with a Conan Exiles
(Hyborian bronze) game skin. The token layer in app/assets/css/main.css has
three ordered layers: a neutral ramp, semantic surface/text/border tokens that
flip with the theme, and the game skin's accent.
Components reference semantic token names only, never raw palette values —
so a re-skin stays one file. The theme is driven by two attributes on <html>:
<html data-theme="dark" data-game="conan">
See docs/design-handoff.md for the full design rationale.
Stack
Nuxt 4 · Vue 3 (Composition API) · TypeScript (strict: true) · Tailwind v4 ·
Pinia · TanStack Table · Lucide icons
Configuration
Node 24 or newer is required (engines in package.json enforces it).
The storage layer is node:sqlite, which is behind a flag on Node 22 and only
stable from Node 24. On an older runtime that import throws
ERR_UNKNOWN_BUILTIN_MODULE — and because the sign-in control lives in the
global header, /api/me is on the critical path of every SSR render, so this
is not a degrade-to-signed-out: it is the whole site down.
Copy .env.example to .env. Every value has a working default for local
development, so the app runs with no .env at all. NUXT_SESSION_SECRET is
required in production — it seals the session cookie, minimum 32 characters.
NUXT_DATABASE_URL points at the SQLite file, defaulting to
./data/forge.db. NUXT_STEAM_API_KEY is optional: without it a signed-in
user shows a bare SteamID, and with it Steam's Web API resolves a display name
and avatar. NUXT_PUBLIC_SITE_URL is normally left unset — see
.env.example for when it isn't.
The NUXT_ prefix is load-bearing in production. Nuxt overrides runtime
config from NUXT_-prefixed variables at server start, and that is the only
mechanism a prebuilt server has. The unprefixed names are read by
nuxt.config.ts, which is evaluated at build time — so SESSION_SECRET
works in development, where the config is re-evaluated on every start, and is
ignored by a production server running an already-built .output.
Ignored quietly, which is the part that costs a day. A missing session secret is a deliberate graceful degrade — every page renders signed-out rather than erroring — so an operator who exports the unprefixed name gets a site that looks healthy, a sign-in button that goes to Steam and comes back, and a database quietly at the default path rather than the configured one. The failure does eventually announce itself, but only at the last step: the Steam return leg answers 500 saying the secret is missing. Everything before that point looks fine. Use the prefixed names; they work in development too.
Running in a container
export NUXT_SESSION_SECRET=$(openssl rand -base64 32)
docker compose up --build
The Dockerfile is an ordinary build — it compiles the source in its own
build stage, so what you build is what you get. It deliberately does not follow
the pattern used by the Swervx services on the same host, which download
prebuilt CI artifacts and can therefore ship stale code from a plain
docker build.
The runtime stage carries only Nuxt's .output, which bundles its own
dependencies — no node_modules, no npm, about 20 MB. The database lives on a
named volume at /data, not in the container's writable layer, so it survives
a redeploy. The healthcheck hits /api/me, which exercises the handler stack
and the session read without opening the database.
Two things to get right on a real deployment, both of which fail without
saying so: the reverse proxy must set force_https or Steam rejects the
assertion, and the environment variables need the NUXT_ prefix described
above. docker-compose.yml refuses to start without NUXT_SESSION_SECRET
rather than degrading to a site where nobody can sign in.