Phase 2: 3D live map driven by API WebSocket
CI / Lint, typecheck, test, build (pull_request) Failing after 9s
CI / Lint, typecheck, test, build (pull_request) Failing after 9s
- apps/live-map/src/hooks/useLiveState.ts: WebSocket subscription with exponential-backoff reconnect, polling fallback, status tracking - apps/live-map/src/scene/Scene.tsx: refactored Three.js scene with per-frame orbit propagation, vessel marker color-coding by owner, orbit-line visibility tied to focus filters, smooth camera follow on selected vessel/body - apps/live-map/src/scene/layout.ts: bodyPositionAt / vesselPositionAt helpers (heliocentric frame, walk up the parent chain), logScale helpers for the system view - apps/live-map/src/scene/color.ts: per-body and per-owner color maps - apps/live-map/src/panels/TimeControls.tsx: play/pause/reverse/reset buttons, ×1/×10/×100/×1k/×10k/×100k speeds, UT scrub slider, live-edge indicator (LIVE / Nh behind / Nh ahead) - apps/live-map/src/panels/VesselList.tsx: vessel sidebar with click- to-track; color-coded by owner (KASA=blue, SPES=orange) - apps/live-map/src/panels/FocusPanel.tsx: planet/moon/vessel orbit visibility toggles - apps/live-map/src/panels/StatusPill.tsx: WS status (LIVE/POLLING/ OFFLINE/STALE), body + vessel + message counts - tests/scene.test.ts: 10 tests for layout helpers (periodicity, vessel-centered positioning, logScale round-trips) End-to-end verified: mock publisher → API → live-map WebSocket → scene re-renders with the new vessel positions and orbits.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* StatusPill — connection + stale-data indicator.
|
||||
*
|
||||
* Shows:
|
||||
* - WS connection state (open / connecting / closed / fallback)
|
||||
* - "Data may be stale" warning if lastUpdate > STALE_THRESHOLD_S
|
||||
* - Number of messages received
|
||||
*/
|
||||
import { Pill } from '@kerbal-rt/ui';
|
||||
import type { ConnectionState } from '../hooks/useLiveState.js';
|
||||
|
||||
export interface StatusPillProps {
|
||||
status: ConnectionState;
|
||||
lastUpdate: string | null;
|
||||
messageCount: number;
|
||||
vesselCount: number;
|
||||
bodyCount: number;
|
||||
}
|
||||
|
||||
const STALE_THRESHOLD_S = 60;
|
||||
|
||||
export function StatusPill({
|
||||
status,
|
||||
lastUpdate,
|
||||
messageCount,
|
||||
vesselCount,
|
||||
bodyCount,
|
||||
}: StatusPillProps) {
|
||||
const isStale = lastUpdate
|
||||
? (Date.now() - new Date(lastUpdate).getTime()) / 1000 > STALE_THRESHOLD_S
|
||||
: true;
|
||||
const tone =
|
||||
status === 'open' && !isStale
|
||||
? 'success'
|
||||
: status === 'connecting' || status === 'fallback'
|
||||
? 'warn'
|
||||
: 'danger';
|
||||
const label =
|
||||
status === 'open'
|
||||
? isStale
|
||||
? 'STALE'
|
||||
: 'LIVE'
|
||||
: status === 'connecting'
|
||||
? 'CONNECTING'
|
||||
: status === 'fallback'
|
||||
? 'POLLING'
|
||||
: 'OFFLINE';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
padding: '0.4rem 0.6rem',
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: 6,
|
||||
backdropFilter: 'blur(4px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
<Pill tone={tone}>{label}</Pill>
|
||||
<Pill tone="neutral">{bodyCount} bodies</Pill>
|
||||
<Pill tone="neutral">{vesselCount} vessels</Pill>
|
||||
<Pill tone="neutral">{messageCount} msgs</Pill>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user