Skip to content

Deploy the dashboard

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.

The narrator dashboard is a static bundle published as GitLab Pages from nerthus.core.dashboard. Deploying it costs one pipeline per push to the default branch — four short Node jobs, a build, and a move — and nothing recurring: Pages serves the bytes, so there is no Worker invocation and no CI minute burned between deploys. It stands nothing up on the fleet either: the app is a client of the API the fleet already publishes — reads for nearly every screen, and writes through the same published routes for the editing plane (lore commits), the workflows and the moderation surface.

Two origins are served from the one deployment:

Origin Default API base Fleet
astral.nerthus.pl https://evocation.nerthus.pl/v1/api production
dev-astral.nerthus.pl https://dev-evocation.nerthus.pl/v1/api dev

Both are Pages custom domains on this project plus a DNS record, the same arrangement cognition.nerthus.pl uses for this site. Neither is a fleet hostname. There is no tunnel behind them, no host, and nothing for fleet:provision to reconcile. Both names are reserved in schema/reserved-names.txt in the fleet contract, so no town slug can ever claim them.

Prerequisites

  • Maintainer on the dashboard project, to set CI/CD variables and Pages domains.
  • Access to the DNS zone for nerthus.pl.
  • The fleet's API already published at evocation.nerthus.pl, per Publish the API. The dashboard is a client; it cannot be the first thing you stand up.
  • A checkout of the fleet contract, to add the origins to the CORS Worker.

1. Grant the two origins at the edge

The daemon sends no CORS headers and has no OPTIONS route, so a browser on astral.nerthus.pl reaches the API only if the edge grants it. Add both origins to ALLOWED_ORIGINS in worker/cors-worker.js in the fleet contract, then republish:

./worker/Publish-NerthusCorsWorker.ps1

The allowlist is a closed set written out rather than pattern-matched, and CORS is granted per origin, so each name needs its own line. Skipping this step produces a dashboard that loads, shows sign-in, and fails every read with a CORS error the browser will not explain.

2. Set the build variable

The bundle bakes its fallback endpoint at build time. Set NERTHUS_API_BASE as a CI/CD variable on the project, or leave the value the build job already carries:

build:
  variables:
    NERTHUS_API_BASE: "https://dev-evocation.nerthus.pl/v1/api"

This fallback is used only where the hostname carries no default of its own — a local vite preview, a review app, or the raw Pages URL reached directly. The two published origins pick their fleet from window.location.hostname, which is why one bundle serves both. A user can repoint the app at runtime from Ustawienia regardless.

The job also exports NERTHUS_DASHBOARD_VERSION from CI_COMMIT_TAG or the short SHA. It is shown in the shell footer so a bug report can name the build it came from.

3. Add the Pages domains and the DNS records

In the project's Deploy → Pages settings add both astral.nerthus.pl and dev-astral.nerthus.pl, then create the DNS records GitLab prints for each, including the _gitlab-pages-verification-code TXT record. Let GitLab issue the certificates.

4. Push to the default branch

The pages job runs only on the default branch. It takes the build job's dist/ artifact and publishes it:

pages:
  script:
    - mv dist public

That single move is why vite.config.ts sets publicDir: false. A directory named public in the repository would turn the move into a nesting — public/dist/index.html, a site whose root is a 404.

Verify it worked

curl -sI https://astral.nerthus.pl/ | head -3
# expect 200 and content-type: text/html

curl -sD - -o /dev/null -X OPTIONS https://evocation.nerthus.pl/v1/api/entities \
  -H 'Origin: https://astral.nerthus.pl' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: authorization'
# expect 204 with access-control-allow-origin: https://astral.nerthus.pl

Then open the site, paste an API key into Ustawienia, and load the Sesje tab. A populated listing means the whole path works: Pages, DNS, the certificate, the edge grant, and the daemon behind it.

When it did not work

Symptom Cause
The site root is a 404 while /dist/ works a public/ directory reached the repository, so mv dist public nested the output
The page loads, every read fails, the console names CORS the origin is missing from ALLOWED_ORIGINS, or the Worker was not republished after the edit
Reads work on dev-astral and fail on astral only one origin was added to the allowlist
The app talks to the wrong fleet the hostname is not in HOST_DEFAULTS in src/config.ts, so it fell back to the build variable
A response header you set is served as text this Pages instance does not honour a _headers file. Response headers for these origins have to come from the edge in front of them
Sign-in through Margonem never completes expected. Margonem's validate endpoint cannot be called from a Nerthus origin — see The dashboard for why, and use an API key or the in-game handoff

See also