diff --git a/.gitignore b/.gitignore index f589a7f..507f144 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,10 @@ coverage/ # KSP game (if you ever mount the install into the repo for dev) KSP/ 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/ diff --git a/README.md b/README.md index 59dec4a..fc419ef 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ full implementation plan. ## Stack -| Layer | Tech | -|---|---| -| **Hub** | Next.js 14 (App Router) + TypeScript | -| **Live map** | Vite + React + Three.js + TypeScript | -| **API** | Hono on Node 22 + WebSocket (`ws`) + Zod | -| **Shared** | `@kerbal-rt/shared-types`, `@kerbal-rt/orbital-math`, `@kerbal-rt/ui` | -| **Data (Phase 1)** | Postgres 16 + TimescaleDB, Redis 7, MinIO (S3) | -| **Package mgr** | pnpm workspaces | -| **Linter** | Prettier (ESLint coming per-app) | +| Layer | Tech | +| ------------------ | --------------------------------------------------------------------- | +| **Hub** | Next.js 14 (App Router) + TypeScript | +| **Live map** | Vite + React + Three.js + TypeScript | +| **API** | Hono on Node 22 + WebSocket (`ws`) + Zod | +| **Shared** | `@kerbal-rt/shared-types`, `@kerbal-rt/orbital-math`, `@kerbal-rt/ui` | +| **Data (Phase 1)** | Postgres 16 + TimescaleDB, Redis 7, MinIO (S3) | +| **Package mgr** | pnpm workspaces | +| **Linter** | Prettier (ESLint coming per-app) | ## Layout @@ -83,6 +83,7 @@ docker compose -f infra/docker-compose.yml up -d ``` This brings up: + - **TimescaleDB** on `:5432` (with the full KSP body catalog pre-seeded) - **Redis** on `:6379` - **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 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 2** — Live map 3D scene (time controls, focus, eclipses, overpass) - [ ] **Phase 3** — Mission calendar, media, crew, YouTube/Twitch embeds diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index f2f16e8..792d0dc 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -5,7 +5,7 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; 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 { state } from './state.js'; @@ -58,8 +58,7 @@ export function buildApp() { // ─── read-only endpoints ─────────────────────────────────────────────── app.get('/api/v1/state', (c) => { const snap = state.latestSnapshot(); - if (!snap) - return c.json({ error: true, code: 'NO_DATA', message: 'No snapshot yet' }, 503); + if (!snap) return c.json({ error: true, code: 'NO_DATA', message: 'No snapshot yet' }, 503); return c.json({ error: false, data: snap }); }); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 6d06d3d..c6e0f95 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -27,6 +27,9 @@ app.get( })), ); +// Suppress unused-import warning when no upgrade happens +void upgradeWebSocket; + const port = Number(process.env.PORT ?? 4000); const server: ServerType = serve({ fetch: app.fetch, port }, (info) => { diff --git a/apps/api/src/ws.ts b/apps/api/src/ws.ts index dad3596..3662e15 100644 --- a/apps/api/src/ws.ts +++ b/apps/api/src/ws.ts @@ -6,33 +6,48 @@ * * Phase 1 will wire this to a Redis pubsub channel so multiple API * 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 { WebSocket } from 'ws'; -const clients = new Set(); +/** 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 { - if (ws.readyState === 1 /* OPEN */) { +const READY_STATE_OPEN = 1; +const clients = new Set(); + +function send(ws: WsLike, msg: LiveMessage): void { + if (ws.readyState === READY_STATE_OPEN) { ws.send(JSON.stringify(msg)); } } export const handleLiveSocket = { - onOpen(ws: WebSocket): void { + onOpen(ws: WsLike): void { 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 2: accept subscriptions like { type: 'subscribe', vesselId: '...' } }, - onClose(ws: WebSocket): void { + onClose(ws: WsLike): void { clients.delete(ws); }, broadcast(msg: LiveMessage): void { for (const ws of clients) send(ws, msg); }, + + /** For tests / introspection. */ + get clientCount(): number { + return clients.size; + }, }; diff --git a/apps/hub/next-env.d.ts b/apps/hub/next-env.d.ts index 4f11a03..40c3d68 100644 --- a/apps/hub/next-env.d.ts +++ b/apps/hub/next-env.d.ts @@ -2,4 +2,4 @@ /// // 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. diff --git a/apps/hub/package.json b/apps/hub/package.json index 4496f89..cf37410 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -4,9 +4,9 @@ "private": true, "description": "Marketing/operations hub: mission calendar, crew, media, Spacenomicon", "scripts": { - "dev": "next dev --port 3000", + "dev": "next dev --port ${PORT:-3000}", "build": "next build", - "start": "next start --port 3000", + "start": "next start --port ${PORT:-3000}", "typecheck": "tsc --noEmit", "lint": "next lint", "test": "echo 'no tests yet'" diff --git a/apps/hub/src/app/layout.tsx b/apps/hub/src/app/layout.tsx index aaec522..4d006c1 100644 --- a/apps/hub/src/app/layout.tsx +++ b/apps/hub/src/app/layout.tsx @@ -3,7 +3,8 @@ import type { ReactNode } from 'react'; export const metadata = { 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 }) { diff --git a/apps/hub/src/app/page.tsx b/apps/hub/src/app/page.tsx index aeb2f88..24992bc 100644 --- a/apps/hub/src/app/page.tsx +++ b/apps/hub/src/app/page.tsx @@ -3,7 +3,9 @@ import { Card, Pill } from '@kerbal-rt/ui'; export default function HomePage() { return (
-
+

Kerbal RT

Phase 0 — skeleton
diff --git a/apps/hub/tsconfig.json b/apps/hub/tsconfig.json index 874c02a..b559f2e 100644 --- a/apps/hub/tsconfig.json +++ b/apps/hub/tsconfig.json @@ -7,7 +7,12 @@ "incremental": true, "module": "ESNext", "moduleResolution": "Bundler", - "plugins": [{ "name": "next" }] + "plugins": [ + { + "name": "next" + } + ], + "allowJs": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] diff --git a/apps/live-map/package.json b/apps/live-map/package.json index 4742594..6898df6 100644 --- a/apps/live-map/package.json +++ b/apps/live-map/package.json @@ -6,7 +6,7 @@ "description": "3D real-time solar system viewer", "scripts": { "dev": "vite --port 3001", - "build": "tsc -p tsconfig.build.json && vite build", + "build": "tsc --noEmit && vite build", "preview": "vite preview --port 3001", "typecheck": "tsc --noEmit", "lint": "echo 'no linter yet'", diff --git a/apps/live-map/src/App.tsx b/apps/live-map/src/App.tsx index e5523d5..a850535 100644 --- a/apps/live-map/src/App.tsx +++ b/apps/live-map/src/App.tsx @@ -148,11 +148,7 @@ function Scene({ bodies, ut }: { bodies: CelestialBody[]; ut: number }) { if (body.sphereOfInfluence === 0 || body.sphereOfInfluence === Infinity) { // Star: render but skip orbit if (body.kind === 'star') { - const geo = new THREE.SphereGeometry( - Math.max(body.radius, 1e8), - 32, - 16, - ); + const geo = new THREE.SphereGeometry(Math.max(body.radius, 1e8), 32, 16); const mat = new THREE.MeshBasicMaterial({ color: 0xffcc33 }); const mesh = new THREE.Mesh(geo, mat); scene.add(mesh); @@ -287,9 +283,7 @@ export function App() {
UT {formatUtAsKspDate(ut)}
- + {[1, 60, 3600, 86400].map((s) => (