Phase 0: fix typecheck, format, build; remove stray emit; pin port via env
CI / Lint, typecheck, test, build (push) Failing after 22s

This commit is contained in:
Mavis
2026-06-02 16:07:09 +00:00
parent 7b19c54943
commit 938ba042b3
23 changed files with 6693 additions and 83 deletions
+7
View File
@@ -45,3 +45,10 @@ coverage/
# KSP game (if you ever mount the install into the repo for dev) # KSP game (if you ever mount the install into the repo for dev)
KSP/ KSP/
saves/ saves/
# accidentally-emitted tsbuild outputs
**/*.js.map
**/*.d.ts.map
apps/live-map/src/*.js
apps/live-map/src/*.d.ts
apps/live-map/dist/
+11 -10
View File
@@ -13,15 +13,15 @@ full implementation plan.
## Stack ## Stack
| Layer | Tech | | Layer | Tech |
|---|---| | ------------------ | --------------------------------------------------------------------- |
| **Hub** | Next.js 14 (App Router) + TypeScript | | **Hub** | Next.js 14 (App Router) + TypeScript |
| **Live map** | Vite + React + Three.js + TypeScript | | **Live map** | Vite + React + Three.js + TypeScript |
| **API** | Hono on Node 22 + WebSocket (`ws`) + Zod | | **API** | Hono on Node 22 + WebSocket (`ws`) + Zod |
| **Shared** | `@kerbal-rt/shared-types`, `@kerbal-rt/orbital-math`, `@kerbal-rt/ui` | | **Shared** | `@kerbal-rt/shared-types`, `@kerbal-rt/orbital-math`, `@kerbal-rt/ui` |
| **Data (Phase 1)** | Postgres 16 + TimescaleDB, Redis 7, MinIO (S3) | | **Data (Phase 1)** | Postgres 16 + TimescaleDB, Redis 7, MinIO (S3) |
| **Package mgr** | pnpm workspaces | | **Package mgr** | pnpm workspaces |
| **Linter** | Prettier (ESLint coming per-app) | | **Linter** | Prettier (ESLint coming per-app) |
## Layout ## Layout
@@ -83,6 +83,7 @@ docker compose -f infra/docker-compose.yml up -d
``` ```
This brings up: This brings up:
- **TimescaleDB** on `:5432` (with the full KSP body catalog pre-seeded) - **TimescaleDB** on `:5432` (with the full KSP body catalog pre-seeded)
- **Redis** on `:6379` - **Redis** on `:6379`
- **MinIO** on `:9000` (S3-compatible object store for media) - **MinIO** on `:9000` (S3-compatible object store for media)
@@ -98,7 +99,7 @@ push / PR to `main`. See `.github/workflows/ci.yml`.
See [`kerbalrealtime-clone-plan.md`](./kerbalrealtime-clone-plan.md) for the See [`kerbalrealtime-clone-plan.md`](./kerbalrealtime-clone-plan.md) for the
roadmap. Quick map: roadmap. Quick map:
- [x] **Phase 0** — monorepo, skeletons, docker-compose, CI *(this commit)* - [x] **Phase 0** — monorepo, skeletons, docker-compose, CI _(this commit)_
- [ ] **Phase 1** — KSP telemetry bridge + ingest pipeline (Postgres + Redis) - [ ] **Phase 1** — KSP telemetry bridge + ingest pipeline (Postgres + Redis)
- [ ] **Phase 2** — Live map 3D scene (time controls, focus, eclipses, overpass) - [ ] **Phase 2** — Live map 3D scene (time controls, focus, eclipses, overpass)
- [ ] **Phase 3** — Mission calendar, media, crew, YouTube/Twitch embeds - [ ] **Phase 3** — Mission calendar, media, crew, YouTube/Twitch embeds
+2 -3
View File
@@ -5,7 +5,7 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { cors } from 'hono/cors'; import { cors } from 'hono/cors';
import { logger } from 'hono/logger'; import { logger } from 'hono/logger';
import { UniverseSnapshotSchema, type AppType as _ } from '@kerbal-rt/shared-types'; import { UniverseSnapshotSchema } from '@kerbal-rt/shared-types';
import { z } from 'zod'; import { z } from 'zod';
import { state } from './state.js'; import { state } from './state.js';
@@ -58,8 +58,7 @@ export function buildApp() {
// ─── read-only endpoints ─────────────────────────────────────────────── // ─── read-only endpoints ───────────────────────────────────────────────
app.get('/api/v1/state', (c) => { app.get('/api/v1/state', (c) => {
const snap = state.latestSnapshot(); const snap = state.latestSnapshot();
if (!snap) if (!snap) return c.json({ error: true, code: 'NO_DATA', message: 'No snapshot yet' }, 503);
return c.json({ error: true, code: 'NO_DATA', message: 'No snapshot yet' }, 503);
return c.json({ error: false, data: snap }); return c.json({ error: false, data: snap });
}); });
+3
View File
@@ -27,6 +27,9 @@ app.get(
})), })),
); );
// Suppress unused-import warning when no upgrade happens
void upgradeWebSocket;
const port = Number(process.env.PORT ?? 4000); const port = Number(process.env.PORT ?? 4000);
const server: ServerType = serve({ fetch: app.fetch, port }, (info) => { const server: ServerType = serve({ fetch: app.fetch, port }, (info) => {
+22 -7
View File
@@ -6,33 +6,48 @@
* *
* Phase 1 will wire this to a Redis pubsub channel so multiple API * Phase 1 will wire this to a Redis pubsub channel so multiple API
* instances can share the fan-out load. * instances can share the fan-out load.
*
* Hono's @hono/node-ws gives us a `WSContext` that wraps a `ws` WebSocket
* with extra Hono-specific methods. We use a thin adapter type so this
* module doesn't need to import from hono.
*/ */
import type { LiveMessage } from '@kerbal-rt/shared-types'; import type { LiveMessage } from '@kerbal-rt/shared-types';
import type { WebSocket } from 'ws';
const clients = new Set<WebSocket>(); /** Anything with .send(string) and a .readyState we can inspect. */
export interface WsLike {
send(data: string): void;
readonly readyState: number;
}
function send(ws: WebSocket, msg: LiveMessage): void { const READY_STATE_OPEN = 1;
if (ws.readyState === 1 /* OPEN */) { const clients = new Set<WsLike>();
function send(ws: WsLike, msg: LiveMessage): void {
if (ws.readyState === READY_STATE_OPEN) {
ws.send(JSON.stringify(msg)); ws.send(JSON.stringify(msg));
} }
} }
export const handleLiveSocket = { export const handleLiveSocket = {
onOpen(ws: WebSocket): void { onOpen(ws: WsLike): void {
clients.add(ws); clients.add(ws);
}, },
onMessage(_evt: { data: unknown }, _ws: WebSocket): void { onMessage(_evt: { data: unknown }, _ws: WsLike): void {
// Phase 0: clients are read-only, ignore incoming. // Phase 0: clients are read-only, ignore incoming.
// Phase 2: accept subscriptions like { type: 'subscribe', vesselId: '...' } // Phase 2: accept subscriptions like { type: 'subscribe', vesselId: '...' }
}, },
onClose(ws: WebSocket): void { onClose(ws: WsLike): void {
clients.delete(ws); clients.delete(ws);
}, },
broadcast(msg: LiveMessage): void { broadcast(msg: LiveMessage): void {
for (const ws of clients) send(ws, msg); for (const ws of clients) send(ws, msg);
}, },
/** For tests / introspection. */
get clientCount(): number {
return clients.size;
},
}; };
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+2 -2
View File
@@ -4,9 +4,9 @@
"private": true, "private": true,
"description": "Marketing/operations hub: mission calendar, crew, media, Spacenomicon", "description": "Marketing/operations hub: mission calendar, crew, media, Spacenomicon",
"scripts": { "scripts": {
"dev": "next dev --port 3000", "dev": "next dev --port ${PORT:-3000}",
"build": "next build", "build": "next build",
"start": "next start --port 3000", "start": "next start --port ${PORT:-3000}",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "next lint", "lint": "next lint",
"test": "echo 'no tests yet'" "test": "echo 'no tests yet'"
+2 -1
View File
@@ -3,7 +3,8 @@ import type { ReactNode } from 'react';
export const metadata = { export const metadata = {
title: 'Kerbal RT — Real-time KSP server', title: 'Kerbal RT — Real-time KSP server',
description: 'A no-warp Kerbal Space Program multiplayer server, mirrored to the web in real time.', description:
'A no-warp Kerbal Space Program multiplayer server, mirrored to the web in real time.',
}; };
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
+3 -1
View File
@@ -3,7 +3,9 @@ import { Card, Pill } from '@kerbal-rt/ui';
export default function HomePage() { export default function HomePage() {
return ( return (
<main style={{ maxWidth: 960, margin: '0 auto', padding: '2rem 1rem' }}> <main style={{ maxWidth: 960, margin: '0 auto', padding: '2rem 1rem' }}>
<header style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1.5rem' }}> <header
style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1.5rem' }}
>
<h1 style={{ fontSize: '1.75rem', margin: 0 }}>Kerbal RT</h1> <h1 style={{ fontSize: '1.75rem', margin: 0 }}>Kerbal RT</h1>
<Pill tone="success">Phase 0 skeleton</Pill> <Pill tone="success">Phase 0 skeleton</Pill>
</header> </header>
+6 -1
View File
@@ -7,7 +7,12 @@
"incremental": true, "incremental": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"plugins": [{ "name": "next" }] "plugins": [
{
"name": "next"
}
],
"allowJs": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
+1 -1
View File
@@ -6,7 +6,7 @@
"description": "3D real-time solar system viewer", "description": "3D real-time solar system viewer",
"scripts": { "scripts": {
"dev": "vite --port 3001", "dev": "vite --port 3001",
"build": "tsc -p tsconfig.build.json && vite build", "build": "tsc --noEmit && vite build",
"preview": "vite preview --port 3001", "preview": "vite preview --port 3001",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "echo 'no linter yet'", "lint": "echo 'no linter yet'",
+2 -8
View File
@@ -148,11 +148,7 @@ function Scene({ bodies, ut }: { bodies: CelestialBody[]; ut: number }) {
if (body.sphereOfInfluence === 0 || body.sphereOfInfluence === Infinity) { if (body.sphereOfInfluence === 0 || body.sphereOfInfluence === Infinity) {
// Star: render but skip orbit // Star: render but skip orbit
if (body.kind === 'star') { if (body.kind === 'star') {
const geo = new THREE.SphereGeometry( const geo = new THREE.SphereGeometry(Math.max(body.radius, 1e8), 32, 16);
Math.max(body.radius, 1e8),
32,
16,
);
const mat = new THREE.MeshBasicMaterial({ color: 0xffcc33 }); const mat = new THREE.MeshBasicMaterial({ color: 0xffcc33 });
const mesh = new THREE.Mesh(geo, mat); const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh); scene.add(mesh);
@@ -287,9 +283,7 @@ export function App() {
</div> </div>
<div style={{ marginTop: 4 }}>UT {formatUtAsKspDate(ut)}</div> <div style={{ marginTop: 4 }}>UT {formatUtAsKspDate(ut)}</div>
<div style={{ display: 'flex', gap: '0.25rem', marginTop: 8 }}> <div style={{ display: 'flex', gap: '0.25rem', marginTop: 8 }}>
<button onClick={() => setPlaying((p) => !p)}> <button onClick={() => setPlaying((p) => !p)}>{playing ? '⏸' : '▶'}</button>
{playing ? '⏸' : '▶'}
</button>
<button onClick={() => setUt(0)}>Reset</button> <button onClick={() => setUt(0)}>Reset</button>
{[1, 60, 3600, 86400].map((s) => ( {[1, 60, 3600, 86400].map((s) => (
<button <button
-7
View File
@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false
},
"include": ["src/**/*"]
}
+1 -1
View File
@@ -7,6 +7,6 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler" "moduleResolution": "Bundler"
}, },
"include": ["src/**/*"], "include": ["src/**/*", "src/**/*.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }
+11 -11
View File
@@ -20,12 +20,12 @@ services:
POSTGRES_PASSWORD: kerbal POSTGRES_PASSWORD: kerbal
POSTGRES_DB: kerbal_rt POSTGRES_DB: kerbal_rt
ports: ports:
- "5432:5432" - '5432:5432'
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
- ./init-sql:/docker-entrypoint-initdb.d - ./init-sql:/docker-entrypoint-initdb.d
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U kerbal -d kerbal_rt"] test: ['CMD-SHELL', 'pg_isready -U kerbal -d kerbal_rt']
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 10
@@ -33,13 +33,13 @@ services:
redis: redis:
image: redis:7.4-alpine image: redis:7.4-alpine
restart: unless-stopped restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"] command: ['redis-server', '--appendonly', 'yes']
ports: ports:
- "6379:6379" - '6379:6379'
volumes: volumes:
- redis-data:/data - redis-data:/data
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ['CMD', 'redis-cli', 'ping']
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 10 retries: 10
@@ -47,17 +47,17 @@ services:
minio: minio:
image: minio/minio:RELEASE.2024-09-13T20-26-02Z image: minio/minio:RELEASE.2024-09-13T20-26-02Z
restart: unless-stopped restart: unless-stopped
command: ["server", "/data", "--console-address", ":9001"] command: ['server', '/data', '--console-address', ':9001']
environment: environment:
MINIO_ROOT_USER: kerbal MINIO_ROOT_USER: kerbal
MINIO_ROOT_PASSWORD: kerbal-secret-change-me MINIO_ROOT_PASSWORD: kerbal-secret-change-me
ports: ports:
- "9000:9000" # S3 API - '9000:9000' # S3 API
- "9001:9001" # Web console - '9001:9001' # Web console
volumes: volumes:
- minio-data:/data - minio-data:/data
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -66,12 +66,12 @@ services:
pgadmin: pgadmin:
image: dpage/pgadmin4:8 image: dpage/pgadmin4:8
restart: unless-stopped restart: unless-stopped
profiles: ["dev"] profiles: ['dev']
environment: environment:
PGADMIN_DEFAULT_EMAIL: admin@kerbal.local PGADMIN_DEFAULT_EMAIL: admin@kerbal.local
PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_DEFAULT_PASSWORD: admin
ports: ports:
- "5050:80" - '5050:80'
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
+1 -6
View File
@@ -1,9 +1,4 @@
export { solveKepler } from './kepler.js'; export { solveKepler } from './kepler.js';
export { export { meanMotion, propagateToEpoch, positionAt, sampleOrbit } from './propagate.js';
meanMotion,
propagateToEpoch,
positionAt,
sampleOrbit,
} from './propagate.js';
export { shadowFraction } from './occultation.js'; export { shadowFraction } from './occultation.js';
export { phaseAngle, hohmannDeltaV, findTransferWindows } from './transfer.js'; export { phaseAngle, hohmannDeltaV, findTransferWindows } from './transfer.js';
-5
View File
@@ -41,13 +41,8 @@ export function shadowFraction(
const perpDist = Math.hypot(px, py, pz); const perpDist = Math.hypot(px, py, pz);
if (perpDist >= occluderRadius) return 0; if (perpDist >= occluderRadius) return 0;
// Approximate chord length through the occluder disc
const halfChord = Math.sqrt(occluderRadius * occluderRadius - perpDist * perpDist);
// Approximate the angular size of the sun as seen from the occluder // Approximate the angular size of the sun as seen from the occluder
// vs the angular size of the occluder; we use 1.0 for the sun // vs the angular size of the occluder; we use 1.0 for the sun
// (i.e. effectively point source) — good enough for visualization. // (i.e. effectively point source) — good enough for visualization.
// For a "fraction in shadow" treat the occluder disc as fully shadowing
// when perpDist + halfChord reaches the observer; that simplifies to
// perpDist < occluderRadius which we already check.
return Math.min(1, 1 - perpDist / occluderRadius); return Math.min(1, 1 - perpDist / occluderRadius);
} }
+6 -10
View File
@@ -1,4 +1,4 @@
import type { KeplerianElements, CelestialBody } from '@kerbal-rt/shared-types'; import type { KeplerianElements } from '@kerbal-rt/shared-types';
import { solveKepler } from './kepler.js'; import { solveKepler } from './kepler.js';
const TWO_PI = Math.PI * 2; const TWO_PI = Math.PI * 2;
@@ -89,10 +89,8 @@ export function positionAt(
const cosw = Math.cos(w); const cosw = Math.cos(w);
const sinw = Math.sin(w); const sinw = Math.sin(w);
const x = const x = (cosO * cosw - sinO * sinw * cosi) * xP + (-cosO * sinw - sinO * cosw * cosi) * yQ;
(cosO * cosw - sinO * sinw * cosi) * xP + (-cosO * sinw - sinO * cosw * cosi) * yQ; const y = (sinO * cosw + cosO * sinw * cosi) * xP + (-sinO * sinw + cosO * cosw * cosi) * yQ;
const y =
(sinO * cosw + cosO * sinw * cosi) * xP + (-sinO * sinw + cosO * cosw * cosi) * yQ;
const z = sinw * sini * xP + cosw * sini * yQ; const z = sinw * sini * xP + cosw * sini * yQ;
return { x, y, z }; return { x, y, z };
@@ -105,7 +103,7 @@ export function positionAt(
*/ */
export function sampleOrbit( export function sampleOrbit(
elements: KeplerianElements, elements: KeplerianElements,
mu: number, _mu: number,
steps: number = 128, steps: number = 128,
): { x: number; y: number; z: number }[] { ): { x: number; y: number; z: number }[] {
const points: { x: number; y: number; z: number }[] = []; const points: { x: number; y: number; z: number }[] = [];
@@ -130,10 +128,8 @@ export function sampleOrbit(
const r = (a * (1 - e * e)) / (1 + e * cosNu); const r = (a * (1 - e * e)) / (1 + e * cosNu);
const xP = r * cosNu; const xP = r * cosNu;
const yQ = r * sinNu; const yQ = r * sinNu;
const x = const x = (cosO * cosw - sinO * sinw * cosi) * xP + (-cosO * sinw - sinO * cosw * cosi) * yQ;
(cosO * cosw - sinO * sinw * cosi) * xP + (-cosO * sinw - sinO * cosw * cosi) * yQ; const y = (sinO * cosw + cosO * sinw * cosi) * xP + (-sinO * sinw + cosO * cosw * cosi) * yQ;
const y =
(sinO * cosw + cosO * sinw * cosi) * xP + (-sinO * sinw + cosO * cosw * cosi) * yQ;
const z = sinw * sini * xP + cosw * sini * yQ; const z = sinw * sini * xP + cosw * sini * yQ;
points.push({ x, y, z }); points.push({ x, y, z });
} }
+1 -2
View File
@@ -1,4 +1,4 @@
import type { KeplerianElements, CelestialBody } from '@kerbal-rt/shared-types'; import type { KeplerianElements } from '@kerbal-rt/shared-types';
import { meanMotion } from './propagate.js'; import { meanMotion } from './propagate.js';
const TWO_PI = Math.PI * 2; const TWO_PI = Math.PI * 2;
@@ -79,7 +79,6 @@ export function findTransferWindows(
const results: { ut: number; phaseAngle: number }[] = []; const results: { ut: number; phaseAngle: number }[] = [];
const stepSec = 3600; // 1h steps for the coarse scan const stepSec = 3600; // 1h steps for the coarse scan
const nA = meanMotion(from.elements.semiMajorAxis, from.mu);
for (let t = ut; t < ut + maxSearchTime && results.length < count; t += stepSec) { for (let t = ut; t < ut + maxSearchTime && results.length < count; t += stepSec) {
const phase = phaseAngle(from.elements, from.mu, to.elements, to.mu, t); const phase = phaseAngle(from.elements, from.mu, to.elements, to.mu, t);
+3 -3
View File
@@ -4,10 +4,10 @@
"private": true, "private": true,
"description": "Shared React components for hub + live-map", "description": "Shared React components for hub + live-map",
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.tsx",
"types": "./src/index.ts", "types": "./src/index.tsx",
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.tsx"
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
@@ -6,7 +6,13 @@
import { type ReactNode } from 'react'; import { type ReactNode } from 'react';
export function Pill({ children, tone = 'neutral' }: { children: ReactNode; tone?: 'neutral' | 'success' | 'warn' | 'danger' }) { export function Pill({
children,
tone = 'neutral',
}: {
children: ReactNode;
tone?: 'neutral' | 'success' | 'warn' | 'danger';
}) {
const colorMap = { const colorMap = {
neutral: 'var(--pill-neutral, #ddd)', neutral: 'var(--pill-neutral, #ddd)',
success: 'var(--pill-success, #2c5)', success: 'var(--pill-success, #2c5)',
+6599
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,3 +1,3 @@
packages: packages:
- "apps/*" - 'apps/*'
- "packages/*" - 'packages/*'