Phase 0: monorepo skeleton (hub, live-map, api, packages, infra, CI)

This commit is contained in:
Mavis
2026-06-02 15:47:28 +00:00
commit 7b19c54943
59 changed files with 2391 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@kerbal-rt/shared-types",
"version": "0.1.0",
"private": true,
"description": "TypeScript types shared between API, hub, and live-map",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "echo 'no linter yet'",
"test": "echo 'no tests yet'",
"build": "tsc"
},
"dependencies": {
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.6.2"
}
}
+203
View File
@@ -0,0 +1,203 @@
/**
* Shared types used across the API, hub, and live-map frontends.
*
* Everything that crosses an HTTP or WebSocket boundary lives here.
* Nothing in this package should import from anywhere else in the
* monorepo — keep it a leaf.
*/
// ─── Celestial bodies ──────────────────────────────────────────────────────
export type BodyKind = 'star' | 'planet' | 'moon' | 'asteroid' | 'comet';
/**
* Keplerian orbital elements at a given epoch.
* Angles are in radians, distances in meters.
*/
export interface KeplerianElements {
/** Semi-major axis (m) */
semiMajorAxis: number;
/** Eccentricity (0=circle, 0..1=ellipse, 1+=parabolic/hyperbolic) */
eccentricity: number;
/** Inclination (rad) */
inclination: number;
/** Longitude of ascending node (rad) */
longitudeOfAscendingNode: number;
/** Argument of periapsis (rad) */
argumentOfPeriapsis: number;
/** Mean anomaly at epoch (rad) */
meanAnomalyAtEpoch: number;
/** Epoch (UT seconds) */
epoch: number;
}
export interface CelestialBody {
id: string;
name: string;
kind: BodyKind;
parentId: string | null;
radius: number; // m
/** Sphere of influence (m); 0 or Infinity for the system root (the star) */
sphereOfInfluence: number;
/** Gravitational parameter μ = G·M (m^3/s^2) */
gravitationalParameter: number;
/** Rotation period (s); 0 for tidally-locked or N/A */
rotationPeriod: number;
/** Axial tilt (rad) */
axialTilt: number;
/** Keplerian elements relative to the parent body */
orbit: KeplerianElements;
}
// ─── Vessels ───────────────────────────────────────────────────────────────
export type VesselSituation =
| 'ORBITING'
| 'ESCAPING'
| 'LANDED'
| 'SPLASHED'
| 'PRELAUNCH'
| 'FLYING'
| 'SUB_ORBITAL'
| 'DOCKED'
| 'UNKNOWN';
export type VesselStatus = 'ACTIVE' | 'DECOMMISSIONED' | 'LOST';
export interface Vessel {
id: string;
name: string;
type: string | null; // e.g. "Probe", "Booster", "Station"
owner: string | null; // e.g. "KASA", "SPES"
situation: VesselSituation;
status: VesselStatus;
/** Keplerian elements relative to the body the vessel is currently orbiting */
orbit: KeplerianElements;
/** ID of the body the vessel's orbit is currently referenced to */
referenceBodyId: string;
createdAt: string; // ISO 8601
retiredAt: string | null;
}
export interface GroundStation {
id: string;
name: string;
bodyId: string;
lat: number; // deg
lon: number; // deg
alt: number; // m above sea level
}
// ─── State snapshots ───────────────────────────────────────────────────────
/**
* A full universe snapshot, published by the KSP telemetry bridge
* on each tick. Frontends use this to render the live map and to
* maintain client-side state.
*/
export interface UniverseSnapshot {
/** KSP universal time, seconds from epoch 0 (year 1, day 1) */
ut: number;
/** Wall-clock time when this snapshot was captured (ISO 8601) */
capturedAt: string;
activeVesselId: string | null;
bodies: CelestialBody[];
vessels: Vessel[];
groundStations: GroundStation[];
}
// ─── Missions & events ─────────────────────────────────────────────────────
export type MissionEventKind =
| 'LAUNCH'
| 'BURN'
| 'FLYBY'
| 'LANDING'
| 'DOCKING'
| 'EVA'
| 'ECLIPSE'
| 'OPERATION'
| 'OTHER';
export interface MissionEvent {
id: number;
mission: string;
vesselId: string | null;
title: string;
description: string | null;
scheduledAt: string; // ISO 8601 wall-clock
utAtEvent: number | null; // KSP UT at the scheduled time, if known
durationSeconds: number | null;
kind: MissionEventKind;
createdBy: string | null;
createdAt: string;
}
export interface Mission {
id: string;
name: string;
agency: string; // "KASA" | "SPES" | other
description: string | null;
startedAt: string;
endedAt: string | null;
vesselIds: string[];
}
// ─── Media ─────────────────────────────────────────────────────────────────
export type MediaKind = 'image' | 'youtube';
export interface MediaItem {
id: number;
kind: MediaKind;
url: string;
thumbnailUrl: string | null;
caption: string | null;
tags: string[];
missionId: string | null;
vesselId: string | null;
createdAt: string;
}
// ─── API envelope ──────────────────────────────────────────────────────────
export interface ApiError {
error: true;
code: string;
message: string;
details?: unknown;
}
export interface ApiOk<T> {
error: false;
data: T;
}
export type ApiResponse<T> = ApiOk<T> | ApiError;
export * from './schemas.js';
// ─── WebSocket protocol ────────────────────────────────────────────────────
/**
* Messages pushed from the API to live-map / hub clients over `/api/v1/live`.
* Clients should ignore unknown message types (forward-compat).
*/
export type LiveMessage =
| { type: 'snapshot'; snapshot: UniverseSnapshot }
| { type: 'vessel_added'; vessel: Vessel }
| { type: 'vessel_updated'; vessel: Vessel }
| { type: 'vessel_removed'; vesselId: string }
| { type: 'event_new'; event: MissionEvent }
| { type: 'event_updated'; event: MissionEvent }
| { type: 'event_removed'; eventId: number }
| { type: 'mission_update'; mission: Mission }
| { type: 'ping'; serverTime: string };
// ─── Constants ─────────────────────────────────────────────────────────────
/** The KSP epoch: year 1, day 1, 00:00:00 UT, in seconds. */
export const KSP_EPOCH_UT = 0;
/** Default polling cadence for clients that don't open a WebSocket. */
export const DEFAULT_SNAPSHOT_TTL_SECONDS = 30;
+111
View File
@@ -0,0 +1,111 @@
/**
* Zod schemas for runtime validation of API payloads. These mirror the
* TypeScript types in `index.ts` and are re-exported so the API can
* validate ingest payloads and the frontends can validate responses
* if they want stronger guarantees.
*/
import { z } from 'zod';
export const KeplerianElementsSchema = z.object({
semiMajorAxis: z.number().nonnegative(),
eccentricity: z.number().min(0),
inclination: z.number(),
longitudeOfAscendingNode: z.number(),
argumentOfPeriapsis: z.number(),
meanAnomalyAtEpoch: z.number(),
epoch: z.number(),
});
export const CelestialBodySchema = z.object({
id: z.string(),
name: z.string(),
kind: z.enum(['star', 'planet', 'moon', 'asteroid', 'comet']),
parentId: z.string().nullable(),
radius: z.number().nonnegative(),
sphereOfInfluence: z.number().nonnegative(),
gravitationalParameter: z.number().nonnegative(),
rotationPeriod: z.number().nonnegative(),
axialTilt: z.number(),
orbit: KeplerianElementsSchema,
});
export const VesselSituationSchema = z.enum([
'ORBITING',
'ESCAPING',
'LANDED',
'SPLASHED',
'PRELAUNCH',
'FLYING',
'SUB_ORBITAL',
'DOCKED',
'UNKNOWN',
]);
export const VesselStatusSchema = z.enum(['ACTIVE', 'DECOMMISSIONED', 'LOST']);
export const VesselSchema = z.object({
id: z.string(),
name: z.string(),
type: z.string().nullable(),
owner: z.string().nullable(),
situation: VesselSituationSchema,
status: VesselStatusSchema,
orbit: KeplerianElementsSchema,
referenceBodyId: z.string(),
createdAt: z.string(),
retiredAt: z.string().nullable(),
});
export const GroundStationSchema = z.object({
id: z.string(),
name: z.string(),
bodyId: z.string(),
lat: z.number(),
lon: z.number(),
alt: z.number(),
});
export const UniverseSnapshotSchema = z.object({
ut: z.number(),
capturedAt: z.string(),
activeVesselId: z.string().nullable(),
bodies: z.array(CelestialBodySchema),
vessels: z.array(VesselSchema),
groundStations: z.array(GroundStationSchema),
});
export const MissionEventSchema = z.object({
id: z.number().int().positive(),
mission: z.string(),
vesselId: z.string().nullable(),
title: z.string(),
description: z.string().nullable(),
scheduledAt: z.string(),
utAtEvent: z.number().nullable(),
durationSeconds: z.number().int().nullable(),
kind: z.enum([
'LAUNCH',
'BURN',
'FLYBY',
'LANDING',
'DOCKING',
'EVA',
'ECLIPSE',
'OPERATION',
'OTHER',
]),
createdBy: z.string().nullable(),
createdAt: z.string(),
});
export const MediaItemSchema = z.object({
id: z.number().int().positive(),
kind: z.enum(['image', 'youtube']),
url: z.string().url(),
thumbnailUrl: z.string().url().nullable(),
caption: z.string().nullable(),
tags: z.array(z.string()),
missionId: z.string().nullable(),
vesselId: z.string().nullable(),
createdAt: z.string(),
});
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}