Spaces: the inside of the world, and a sun that is actually where it should be
Ten agents wrote this in parallel against CONTRACT.md, which exists because the five design agents before them collided on fifteen blocking points — four files specified twice with incompatible contents, three separate backends for one box, and `Environment` exported twice meaning different things. What landed: a Stage owning only the renderer and the loop, with the city and an office as two scenes over it. They cannot share one — San Francisco is ~94 m per scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and the city is paused rather than disposed on the way in, because rebuilding its 336,864-point heightfield costs about a second on the way back out. Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six seats, and it is the file a self-hoster copies. Walls are a segment list with 1-D openings, so doors and windows are holes punched in a wall rather than placed objects, and the pass that splits a wall around its openings hands the walk-mode collider its segments for free. The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at all — not even three.js — so time of day keeps working on a laptop in a field. Verified against known values: 75.45 degrees at the June solstice in SF, 28.79 at December, sunset at 03:15Z. The first screenshot after wiring it was a black rectangle, which turned out to be correct: it was midnight in San Francisco. Presence binds to a seat id and never to a coordinate. The pack knows where `eng-04` is; who is sitting in it is private data behind an API. Same shape as the marker rule, one level in. Two corrections to ARCHITECTURE.md are in here. Containment does not discharge ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database wherever the rows live, so the rule is about the geocoder (US Census, public domain) and not the storage. And a person at a desk is not a Marker; markers are geographic. One contract gap surfaced only in a screenshot: two agents read `height` on a viewpoint differently, so the establishing shot aimed at empty air fourteen metres above the roof. It now means what the same field means for a city. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* The one server.
|
||||
*
|
||||
* One Fastify instance on `127.0.0.1:8431` serving `/api/v1/*`, behind Caddy,
|
||||
* with one systemd unit and one Caddy snippet. Three separate backends were
|
||||
* designed for this box — three ports, three frameworks, three deploy files that
|
||||
* would have overwritten each other — and this is the one that replaces them.
|
||||
* Weather, flights, markers and offices are route modules here, not services on
|
||||
* ports of their own. CONTRACT.md §5.
|
||||
*
|
||||
* `buildApp` never listens. Tests build one and use `inject()`; `index.ts` builds
|
||||
* one and binds it.
|
||||
*/
|
||||
|
||||
import Fastify, { type FastifyInstance } from "fastify";
|
||||
import { registerCachePolicy } from "./cache.ts";
|
||||
import { loadConfig, type Config } from "./config.ts";
|
||||
import { registerFlights } from "./routes/flights.ts";
|
||||
import { registerHealth } from "./routes/health.ts";
|
||||
import { registerMarkers } from "./routes/markers.ts";
|
||||
import { registerOffices } from "./routes/offices.ts";
|
||||
import { registerWeather } from "./routes/weather.ts";
|
||||
import { createServices } from "./services.ts";
|
||||
import type { ErrorBody } from "../../src/server/wire.ts";
|
||||
|
||||
export function buildApp(config: Config = loadConfig()): FastifyInstance {
|
||||
const app = Fastify({
|
||||
logger: { level: config.logLevel },
|
||||
// Caddy is the only thing that talks to this socket, so its X-Forwarded-For
|
||||
// is the client address. Nothing else can reach the port to forge one.
|
||||
trustProxy: true,
|
||||
});
|
||||
|
||||
// One loud line per demotion, at boot, before anything can be served. The
|
||||
// health body carries the same list for anyone without log access.
|
||||
for (const line of config.degraded) {
|
||||
app.log.warn(`TERA DEGRADED: ${line}`);
|
||||
}
|
||||
|
||||
registerCachePolicy(app);
|
||||
registerCors(app, config.corsOrigins);
|
||||
|
||||
const services = createServices(config, app.log);
|
||||
registerHealth(app, services);
|
||||
registerFlights(app, services);
|
||||
registerWeather(app, services);
|
||||
registerMarkers(app, services);
|
||||
registerOffices(app, services);
|
||||
|
||||
app.setNotFoundHandler(async (_req, reply) => {
|
||||
const body: ErrorBody = { error: "not_found", message: "No such route." };
|
||||
return reply.code(404).send(body);
|
||||
});
|
||||
|
||||
app.setErrorHandler(async (err, _req, reply) => {
|
||||
app.log.error({ err }, "unhandled error");
|
||||
const body: ErrorBody = { error: "internal", message: "Something went wrong." };
|
||||
return reply.code(500).send(body);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS, by hand.
|
||||
*
|
||||
* The default is an empty allowlist — the browser build is served from the same
|
||||
* origin through Caddy, so nothing needs this until somebody runs the dev server
|
||||
* on a different port and sets `TERA_CORS_ORIGIN`. A plugin whose entire job is
|
||||
* fifteen lines of header-setting is not worth the dependency, and the fifteen
|
||||
* lines being visible here is worth something on a route that decides who may
|
||||
* read the API from where.
|
||||
*/
|
||||
function registerCors(app: FastifyInstance, allowed: string[]): void {
|
||||
if (allowed.length === 0) return;
|
||||
|
||||
app.addHook("onRequest", async (req, reply) => {
|
||||
const origin = req.headers.origin;
|
||||
if (typeof origin !== "string" || !allowed.includes(origin)) return;
|
||||
|
||||
reply.header("access-control-allow-origin", origin);
|
||||
reply.header("access-control-allow-headers", "authorization, content-type");
|
||||
reply.header("access-control-allow-methods", "GET, OPTIONS");
|
||||
// Whatever the response ends up being, it depended on the Origin header.
|
||||
reply.header("vary", "Origin");
|
||||
|
||||
if (req.method === "OPTIONS") await reply.code(204).send();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user