Phase 0: monorepo skeleton (hub, live-map, api, packages, infra, CI)
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# Docker Compose for the kerbal-rt data tier.
|
||||
#
|
||||
# Phase 0 ships this as a deliverable but doesn't run anything here
|
||||
# (no Docker available in the dev sandbox). Bring it up with:
|
||||
#
|
||||
# docker compose -f infra/docker-compose.yml up -d
|
||||
#
|
||||
# Then point the API at it via .env (see .env.example).
|
||||
#
|
||||
# Replaces the in-memory StateStore in apps/api/src/state.ts.
|
||||
# Phase 1 wires Postgres + Timescale (telemetry), Redis (pubsub),
|
||||
# and MinIO (media) into the API.
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: timescale/timescaledb:2.16.1-pg16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: kerbal
|
||||
POSTGRES_PASSWORD: kerbal
|
||||
POSTGRES_DB: kerbal_rt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- ./init-sql:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U kerbal -d kerbal_rt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7.4-alpine
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2024-09-13T20-26-02Z
|
||||
restart: unless-stopped
|
||||
command: ["server", "/data", "--console-address", ":9001"]
|
||||
environment:
|
||||
MINIO_ROOT_USER: kerbal
|
||||
MINIO_ROOT_PASSWORD: kerbal-secret-change-me
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Web console
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# pgAdmin is convenient in dev; comment out for prod
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:8
|
||||
restart: unless-stopped
|
||||
profiles: ["dev"]
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@kerbal.local
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
ports:
|
||||
- "5050:80"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
@@ -0,0 +1,17 @@
|
||||
# Grafana provisioning: auto-load a TimescaleDB datasource on container start.
|
||||
# Mount this file at /etc/grafana/provisioning/datasources/timescaledb.yml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: TimescaleDB
|
||||
type: postgres
|
||||
url: postgres:5432
|
||||
database: kerbal_rt
|
||||
user: kerbal
|
||||
secureJsonData:
|
||||
password: kerbal
|
||||
jsonData:
|
||||
sslmode: disable
|
||||
postgresVersion: 1600
|
||||
timescaledb: true
|
||||
isDefault: true
|
||||
@@ -0,0 +1,84 @@
|
||||
-- Schema bootstrap for the kerbal-rt Postgres + Timescale install.
|
||||
-- Runs automatically on first container start.
|
||||
--
|
||||
-- See kerbalrealtime-clone-plan.md §Component B for the full data model.
|
||||
-- This is the Phase 0 minimum; Phase 1 fills in the rest.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Bodies (celestial body catalog; small, mostly static)
|
||||
CREATE TABLE IF NOT EXISTS bodies (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('star','planet','moon','asteroid','comet')),
|
||||
parent_id TEXT REFERENCES bodies(id),
|
||||
radius DOUBLE PRECISION NOT NULL,
|
||||
sphere_of_influence DOUBLE PRECISION NOT NULL,
|
||||
gravitational_parameter DOUBLE PRECISION NOT NULL,
|
||||
rotation_period DOUBLE PRECISION NOT NULL,
|
||||
axial_tilt DOUBLE PRECISION NOT NULL,
|
||||
elements JSONB NOT NULL
|
||||
);
|
||||
|
||||
-- Vessels (slowly-changing dimension)
|
||||
CREATE TABLE IF NOT EXISTS vessels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT,
|
||||
owner TEXT,
|
||||
status TEXT NOT NULL CHECK (status IN ('ACTIVE','DECOMMISSIONED','LOST')) DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
retired_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS vessels_status_idx ON vessels(status);
|
||||
|
||||
-- Telemetry snapshots (high-volume time series)
|
||||
CREATE TABLE IF NOT EXISTS telemetry_snapshots (
|
||||
ts TIMESTAMPTZ NOT NULL,
|
||||
ut DOUBLE PRECISION NOT NULL,
|
||||
payload JSONB NOT NULL
|
||||
);
|
||||
SELECT create_hypertable('telemetry_snapshots', 'ts', if_not_exists => TRUE);
|
||||
CREATE INDEX IF NOT EXISTS telemetry_snapshots_ut_idx ON telemetry_snapshots(ut DESC);
|
||||
|
||||
-- Mission events
|
||||
CREATE TABLE IF NOT EXISTS mission_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
mission TEXT NOT NULL,
|
||||
vessel_id TEXT REFERENCES vessels(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
scheduled_at TIMESTAMPTZ NOT NULL,
|
||||
ut_at_event DOUBLE PRECISION,
|
||||
duration_s INTEGER,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('LAUNCH','BURN','FLYBY','LANDING','DOCKING','EVA','ECLIPSE','OPERATION','OTHER')),
|
||||
created_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mission_events_scheduled_idx ON mission_events(scheduled_at);
|
||||
|
||||
-- Media
|
||||
CREATE TABLE IF NOT EXISTS media (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('image','youtube')),
|
||||
url TEXT NOT NULL,
|
||||
thumbnail_url TEXT,
|
||||
caption TEXT,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
mission_id TEXT,
|
||||
vessel_id TEXT REFERENCES vessels(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS media_tags_idx ON media USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS media_created_idx ON media(created_at DESC);
|
||||
|
||||
-- Ground stations
|
||||
CREATE TABLE IF NOT EXISTS ground_stations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
body_id TEXT NOT NULL REFERENCES bodies(id),
|
||||
lat DOUBLE PRECISION NOT NULL,
|
||||
lon DOUBLE PRECISION NOT NULL,
|
||||
alt DOUBLE PRECISION NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Seed data: the Kerbol system (KSP stock).
|
||||
-- Numbers sourced from the KSP wiki and the in-game config files.
|
||||
-- Gravitational parameter μ in m^3/s^2.
|
||||
-- This is enough to render the system in the live map before
|
||||
-- the real telemetry bridge is wired in.
|
||||
|
||||
INSERT INTO bodies (id, name, kind, parent_id, radius, sphere_of_influence, gravitational_parameter, rotation_period, axial_tilt, elements) VALUES
|
||||
('kerbol', 'Kerbol', 'star', NULL, 261600000, 'Infinity'::float8::float8, 1.172332794e18, 432000, 0, '{"semiMajorAxis":0,"eccentricity":0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":0,"epoch":0}'),
|
||||
('moho', 'Moho', 'planet', 'kerbol', 250000, 9646663, 1.686842e11, 1210000, 0.05, '{"semiMajorAxis":5263138304,"eccentricity":0.2,"inclination":0.075,"longitudeOfAscendingNode":1.5,"argumentOfPeriapsis":2.8,"meanAnomalyAtEpoch":1.0,"epoch":0}'),
|
||||
('eve', 'Eve', 'planet', 'kerbol', 700000, 85109365, 8.1717302e12, 80500, 0.1, '{"semiMajorAxis":9832684544,"eccentricity":0.01,"inclination":0.1,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":3.14,"epoch":0}'),
|
||||
('kerbin', 'Kerbin', 'planet', 'kerbol', 600000, 84159286, 3.5316e12, 21600, 0, '{"semiMajorAxis":13599840256,"eccentricity":0.05,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":0.7,"epoch":0}'),
|
||||
('duna', 'Duna', 'planet', 'kerbol', 320000, 47921949, 3.0136321e11, 65518, 0.06, '{"semiMajorAxis":20726155264,"eccentricity":0.05,"inclination":0.02,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":1.5,"epoch":0}'),
|
||||
('dres', 'Dres', 'planet', 'kerbol', 138000, 32832840, 2.1484489e10, 34800, 0.08, '{"semiMajorAxis":40839348203,"eccentricity":0.14,"inclination":0.08,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":2.1,"epoch":0}'),
|
||||
('jool', 'Jool', 'planet', 'kerbol', 6000000, 2450055988, 2.82528e14, 36000, 0.05, '{"semiMajorAxis":68773560320,"eccentricity":0.05,"inclination":0.04,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":4.2,"epoch":0}'),
|
||||
('eeloo', 'Eeloo', 'planet', 'kerbol', 210000, 119082940, 7.4410815e10, 19460, 0.1, '{"semiMajorAxis":90118820000,"eccentricity":0.26,"inclination":0.13,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":5.5,"epoch":0}'),
|
||||
('gilly', 'Gilly', 'moon', 'eve', 13000, 126123, 8289449.8, 28260, 0.05, '{"semiMajorAxis":31500000,"eccentricity":0.18,"inclination":0.2,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":0.5,"epoch":0}'),
|
||||
('mun', 'Mun', 'moon', 'kerbin', 200000, 2429559, 6.514e10, 138984, 0, '{"semiMajorAxis":12000000,"eccentricity":0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":1.0,"epoch":0}'),
|
||||
('minmus', 'Minmus', 'moon', 'kerbin', 60000, 2247428, 1.765e9, 40400, 0.04, '{"semiMajorAxis":47000000,"eccentricity":0.0,"inclination":0.075,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":2.5,"epoch":0}'),
|
||||
('ike', 'Ike', 'moon', 'duna', 130000, 1048598, 1.856e9, 65518, 0.05, '{"semiMajorAxis":3200000,"eccentricity":0.0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":1.5,"epoch":0}'),
|
||||
('laythe', 'Laythe', 'moon', 'jool', 500000, 3723645, 1.840e11, 52980, 0, '{"semiMajorAxis":27184000,"eccentricity":0.0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":2.0,"epoch":0}'),
|
||||
('vall', 'Vall', 'moon', 'jool', 240000, 2406401, 2.061e10, 106200, 0, '{"semiMajorAxis":43152000,"eccentricity":0.0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":2.5,"epoch":0}'),
|
||||
('tylo', 'Tylo', 'moon', 'jool', 375000, 10856418, 2.122e11, 84600, 0, '{"semiMajorAxis":68500000,"eccentricity":0.0,"inclination":0,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":3.0,"epoch":0}'),
|
||||
('bop', 'Bop', 'moon', 'jool', 65000, 1220600, 2.486e8, 360, 0.05, '{"semiMajorAxis":128500000,"eccentricity":0.23,"inclination":0.2,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":4.0,"epoch":0}'),
|
||||
('pol', 'Pol', 'moon', 'jool', 44000, 1042138, 7.214e7, 340, 0.05, '{"semiMajorAxis":179890000,"eccentricity":0.17,"inclination":0.15,"longitudeOfAscendingNode":0,"argumentOfPeriapsis":0,"meanAnomalyAtEpoch":4.5,"epoch":0}')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
radius = EXCLUDED.radius,
|
||||
sphere_of_influence = EXCLUDED.sphere_of_influence,
|
||||
gravitational_parameter = EXCLUDED.gravitational_parameter,
|
||||
rotation_period = EXCLUDED.rotation_period,
|
||||
axial_tilt = EXCLUDED.axial_tilt,
|
||||
elements = EXCLUDED.elements;
|
||||
|
||||
-- Sample ground station: the project's "Montana DSN" (real site has this).
|
||||
INSERT INTO ground_stations (id, name, body_id, lat, lon, alt) VALUES
|
||||
('montana', 'Montana DSN', 'kerbin', 47.0, -110.0, 1200)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,64 @@
|
||||
# nginx config for production. Phase 0 ships as deliverable; deploy in Phase 6.
|
||||
#
|
||||
# Routes:
|
||||
# / -> hub (Next.js) on :3000
|
||||
# /live-map/ -> live-map (Vite preview) on :3001
|
||||
# /api/ -> API (Hono) on :4000
|
||||
# /ws -> API WebSocket upgrade for /api/v1/live
|
||||
|
||||
upstream hub {
|
||||
server 127.0.0.1:3000;
|
||||
}
|
||||
upstream map {
|
||||
server 127.0.0.1:3001;
|
||||
}
|
||||
upstream api {
|
||||
server 127.0.0.1:4000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 25m;
|
||||
|
||||
# Hub
|
||||
location / {
|
||||
proxy_pass http://hub;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Live map (mounted under /live-map/ so assets resolve)
|
||||
location /live-map/ {
|
||||
proxy_pass http://map/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# API + WebSocket
|
||||
location /api/ {
|
||||
proxy_pass http://api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 86400; # keep WebSockets open
|
||||
}
|
||||
}
|
||||
|
||||
# Map for WebSocket connection upgrade
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
Reference in New Issue
Block a user