From 44c5a79424fbad095322ce11833dc359514762f0 Mon Sep 17 00:00:00 2001
From: Karti Tripathi
Date: Wed, 5 Aug 2026 03:13:32 -0700
Subject: [PATCH] SoCal, the whole bay, a moon, and gates that actually run
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Six agents in parallel, and the two city packs independently reported the same
blocker: `focusRegions` and `coarseFactor` existed on the `City` type and
nothing implemented them. Uniform lattices would have been 2.9M points for
Southern California and 3.7M for the expanded bay. Both packs were unloadable
as written.
`buildAxis` is the answer, and it is honest about its limits: refinement is per
axis, not per rectangle, so a focus region sharpens its whole row *and* its
whole column. Two regions at opposite corners refine nearly everything between
them. Measured, not guessed — the bay went 0.53M points with one region and
1.64M with three, for detail nobody is looking at from a board this wide. One
region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s.
Then three things that were only ever right because San Francisco was the only
city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a
230-unit board; the bay is 1003 units across and the camera physically could
not retreat far enough to frame it. Fog distances were scene units pinned to
the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest
weather, which over ninety-four kilometres of bay correctly hides three
quarters of it — the night view was a black rectangle for a completely
reasonable reason. All three now derive from the board.
The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a
physical ratio of one to four hundred thousand. What is being reproduced is
what a moonlit night looks like on a screen in a lit room.
The CI gate caught itself, which is the part worth keeping. Port 8431 was
already held by a server from an earlier session, so the boot check polled a
healthy stranger while the process it started died on EADDRINUSE. It now
refuses to run rather than pass.
Co-Authored-By: Claude Opus 5
---
.gitea/workflows/gates.yml | 119 ++
.gitignore | 19 +
index.html | 12 +
public/login.html | 148 ++
scripts/check-no-binaries.mjs | 160 ++
scripts/check-zero-config-boot.mjs | 346 +++
server/src/app.ts | 2 +
server/src/auth/index.ts | 72 +-
server/src/auth/password.ts | 206 ++
server/src/config.ts | 117 +-
server/src/routes/session.ts | 215 ++
server/src/test/session.test.ts | 307 +++
src/adapters/README.md | 105 +
src/adapters/http.ts | 333 +++
src/adapters/sample.ts | 308 +++
src/cities/sf.ts | 1694 ++++++++++++++-
src/cities/socal.ts | 3152 ++++++++++++++++++++++++++++
src/engine/atmosphere.ts | 540 ++++-
src/engine/blocks.ts | 50 +
src/engine/flights.ts | 470 ++++-
src/engine/nightlights.ts | 572 +++++
src/engine/scene.ts | 46 +-
src/engine/terrain.ts | 7 +-
src/engine/world.ts | 183 +-
src/main.ts | 283 ++-
25 files changed, 9246 insertions(+), 220 deletions(-)
create mode 100644 .gitea/workflows/gates.yml
create mode 100644 public/login.html
create mode 100644 scripts/check-no-binaries.mjs
create mode 100644 scripts/check-zero-config-boot.mjs
create mode 100644 server/src/auth/password.ts
create mode 100644 server/src/routes/session.ts
create mode 100644 server/src/test/session.test.ts
create mode 100644 src/adapters/README.md
create mode 100644 src/adapters/http.ts
create mode 100644 src/adapters/sample.ts
create mode 100644 src/cities/socal.ts
create mode 100644 src/engine/nightlights.ts
diff --git a/.gitea/workflows/gates.yml b/.gitea/workflows/gates.yml
new file mode 100644
index 0000000..b57fe85
--- /dev/null
+++ b/.gitea/workflows/gates.yml
@@ -0,0 +1,119 @@
+# The gates.
+#
+# This repo makes two claims about itself — that it is Apache-2.0 clean, and that
+# a stranger can self-host it with no account, no key and no network — and until
+# this file existed nothing checked either one. Both had been verified per-lens,
+# in a unit test or by hand on a box that already had everything, and never once
+# end to end on an empty environment. A design critic put it exactly right:
+# until one of these jobs exists, self-hostable is a design intention, not a
+# property.
+#
+# Three jobs, one for each claim that can actually be measured:
+#
+# clean-clone a fresh checkout installs, builds and passes its tests
+# zero-config-boot the API answers health when handed nothing at all
+# no-binary-art src/** carries no committed binary assets
+#
+# They are deliberately independent and run in parallel: a broken build should
+# not hide a licensing regression.
+#
+# This runs on Gitea Actions, not GitHub. The syntax is GitHub-compatible and the
+# `actions/*` steps resolve through the instance's configured action registry.
+
+name: gates
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ workflow_dispatch:
+
+# A superseded push has nothing to tell us, and these jobs build a Docker image
+# and bind a port between them.
+concurrency:
+ group: gates-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # ---- Job 1: a stranger clones this and it works ----
+ #
+ # The whole job is the dev-kit promise from CONTRACT.md §0. Note what is absent
+ # and is meant to stay absent: no `env:` block, no `secrets.*` anywhere, and no
+ # dependency cache. The cache is the interesting omission — a warm cache would
+ # make this job pass on a lockfile that no longer resolves from a clean state,
+ # which is precisely the failure a stranger would hit first.
+ clean-clone:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ # The server runs TypeScript directly by stripping types at load, so
+ # the floor is real: server/package.json says >=22.18. 24 is what the
+ # image in server/Dockerfile uses.
+ node-version: "24"
+
+ - name: npm ci
+ run: npm ci
+
+ - name: npm run build
+ # tsc --noEmit, then vite build. A dynamic import of an uninstalled
+ # package fails here, which is the reason the dependency policy is an
+ # allowlist rather than a count — see CONTRACT.md §6.
+ run: npm run build
+
+ - name: server tests
+ run: npm test --workspace @lumbridge/tera-api
+
+ # ---- Job 2: the box boots on nothing ----
+ #
+ # Two independent server designs defaulted the weather source to a provider
+ # that needs a contact string and then failed hard without one, which breaks
+ # the only acceptance test this repo has. This job exists specifically to catch
+ # that class of bug, and it passes today.
+ #
+ # The gate starts the real entry point under a genuinely empty environment
+ # rather than `docker compose up`, which is the wording in CONTRACT.md §0. The
+ # property asserted is identical — the compose file adds only TERA_HOST and
+ # container hardening, and every other variable in it carries a `:-` default so
+ # an empty environment resolves it to the empty string — while a job that needs
+ # docker-in-docker present on the runner would be measuring the runner instead
+ # of the repo. `node scripts/check-zero-config-boot.mjs --compose` runs the
+ # literal compose form for anyone who has a Docker.
+ zero-config-boot:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+
+ - name: npm ci
+ run: npm ci
+
+ - name: health on an empty environment
+ run: node scripts/check-zero-config-boot.mjs
+
+ # ---- Job 3: no committed binary art ----
+ #
+ # Scoped strictly to tracked files under src/**, because a self-hoster is told
+ # to put their own legally-clean art in public/props/ and friends and an
+ # earlier design would have failed their build for doing exactly that. The
+ # script enumerates with `git ls-files` and has no filesystem-walk fallback.
+ # See CONTRACT.md §7.
+ #
+ # No install step: the check is dependency-free on purpose, so it stays
+ # runnable by hand and cannot be broken by a bad lockfile.
+ no-binary-art:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+
+ - name: no binary art under src/
+ run: node scripts/check-no-binaries.mjs
diff --git a/.gitignore b/.gitignore
index 274275c..9d4e091 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,22 @@ dist/
# runtime and cached in the browser. An Apache-2.0 repo containing them would
# be relicensing marks it does not own. See ARCHITECTURE.md §3.1.
public/logos/
+
+# ---- Self-hoster space ----
+#
+# Yours, not ours. Drop your own props, kits, office packs and notes here and
+# nothing in this repo will look at them, complain about them or ship them.
+#
+# This is the other half of the no-binary-art gate. That check
+# (scripts/check-no-binaries.mjs) enumerates with `git ls-files` and is strict
+# only over src/**, so your own legally-clean .glb sitting in public/props/ is
+# invisible to it and cannot fail your build — an earlier design walked the
+# working tree and would have. Ignoring these paths is what makes that promise
+# hold by default rather than by everybody remembering. See CONTRACT.md §7.
+#
+# Worth knowing before you write one: docs/ is in here too, so documentation
+# committed to this repo lives at the root or beside the code it describes.
+public/props/
+public/kits/
+public/offices/
+docs/
diff --git a/index.html b/index.html
index cb6b325..848e554 100644
--- a/index.html
+++ b/index.html
@@ -37,6 +37,16 @@
.enter:hover { background: #ffc555; }
.scrub { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.45rem; }
.scrub input { flex: 1; accent-color: #f2b134; height: 14px; }
+ .cities { display: flex; gap: 3px; }
+ .city { flex: 1; font: inherit; font-size: 11px; padding: 0.35rem; cursor: pointer;
+ border: 0; border-radius: 5px; background: rgba(8,12,16,0.55); color: rgba(255,255,255,0.6);
+ backdrop-filter: blur(6px); }
+ .city:hover { background: rgba(255,255,255,0.14); }
+ .city.active { background: rgba(242,177,52,0.22); color: #ffd68a; }
+ .source { position: fixed; left: 1rem; bottom: 1rem; font-size: 10px;
+ color: rgba(255,255,255,0.4); background: rgba(8,12,16,0.5); padding: 0.25rem 0.5rem;
+ border-radius: 4px; backdrop-filter: blur(6px); }
+ .source.live { color: #7ee08a; }
.scrub button { font: inherit; font-size: 9px; text-transform: uppercase;
letter-spacing: 0.08em; padding: 0.15rem 0.35rem; cursor: pointer; border: 0;
border-radius: 3px; background: rgba(255,255,255,0.13); color: rgba(255,255,255,0.7); }
@@ -54,11 +64,13 @@
+
+