feat(monitor/frontend): ISS panel (live position + next-pass placeholder for phase 6)
Four-row layout: Lat/Lon (degree symbol, mono), Alt (km, mono tabular), Vel (km/h with thousands separator, mono tabular), Next Pass (— stub). Reads snapshot.iss_position directly (no per-panel store — current position is the only thing the panel cares about, and the wheretheiss.at collector pushes a complete state every 10s, so latest-event semantics match what the panel needs). Real shape verified: latitude/longitude/ altitude/velocity/timestamp/fetched_at — all numeric except fetched_at ISO string. Stale threshold 30s (3x the 10s poll cadence). Next Pass row stays "—" muted --fg-3 — Phase 6 wires the SGP4-driven user-observer-location pass prediction per spec §10.4. No "sign in to add observer" CTA: operator UI doesn't onboard. Plan 5 Task 21 Design QA: degree symbol + mono coords, alt/vel mono tabular, no decorative ISS-tracing widget (the centerpiece globe owns that), next-pass row visible-but-muted (never hidden).
This commit is contained in:
parent
f0bb0dcb8d
commit
43df53f845
|
|
@ -6,6 +6,7 @@ import { BTCPanel } from '@/pages/panels/BTCPanel'
|
||||||
import { CVEVelocityPanel } from '@/pages/panels/CVEVelocityPanel'
|
import { CVEVelocityPanel } from '@/pages/panels/CVEVelocityPanel'
|
||||||
import { DShieldPanel } from '@/pages/panels/DShieldPanel'
|
import { DShieldPanel } from '@/pages/panels/DShieldPanel'
|
||||||
import { ETHPanel } from '@/pages/panels/ETHPanel'
|
import { ETHPanel } from '@/pages/panels/ETHPanel'
|
||||||
|
import { ISSPanel } from '@/pages/panels/ISSPanel'
|
||||||
import { KEVPanel } from '@/pages/panels/KEVPanel'
|
import { KEVPanel } from '@/pages/panels/KEVPanel'
|
||||||
import { RansomwarePanel } from '@/pages/panels/RansomwarePanel'
|
import { RansomwarePanel } from '@/pages/panels/RansomwarePanel'
|
||||||
import { SpaceWeatherPanel } from '@/pages/panels/SpaceWeatherPanel'
|
import { SpaceWeatherPanel } from '@/pages/panels/SpaceWeatherPanel'
|
||||||
|
|
@ -33,6 +34,7 @@ export function Dashboard(): React.ReactElement {
|
||||||
<BTCPanel />
|
<BTCPanel />
|
||||||
<ETHPanel />
|
<ETHPanel />
|
||||||
<SpaceWeatherPanel />
|
<SpaceWeatherPanel />
|
||||||
|
<ISSPanel />
|
||||||
</aside>
|
</aside>
|
||||||
<section className={styles.center}>
|
<section className={styles.center}>
|
||||||
<Globe />
|
<Globe />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// ISSPanel.module.scss
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--type-body);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: var(--type-label);
|
||||||
|
letter-spacing: var(--letter-spacing-label);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--fg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--fg-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--fg-3);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
// ©AngelaMos | 2026
|
||||||
|
// ISSPanel.tsx
|
||||||
|
|
||||||
|
import { useSnapshot } from '@/api/snapshot'
|
||||||
|
import styles from './ISSPanel.module.scss'
|
||||||
|
import { Panel } from './Panel'
|
||||||
|
|
||||||
|
const STALE_AFTER_MS = 30_000
|
||||||
|
const COORDS_DECIMALS = 2
|
||||||
|
const ALT_DECIMALS = 0
|
||||||
|
|
||||||
|
interface IssPositionData {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
altitude: number
|
||||||
|
velocity: number
|
||||||
|
timestamp: number
|
||||||
|
fetched_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ISSPanel(): React.ReactElement {
|
||||||
|
const { data } = useSnapshot()
|
||||||
|
const iss = data?.iss_position as IssPositionData | undefined
|
||||||
|
|
||||||
|
const lastTickAt = iss?.fetched_at
|
||||||
|
? new Date(iss.fetched_at).getTime()
|
||||||
|
: undefined
|
||||||
|
const isStale =
|
||||||
|
lastTickAt !== undefined
|
||||||
|
? Date.now() - lastTickAt > STALE_AFTER_MS
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
title="ISS"
|
||||||
|
subtitle="POSITION"
|
||||||
|
rawHref="https://wheretheiss.at/"
|
||||||
|
rawLabel="wheretheiss.at"
|
||||||
|
isStale={isStale}
|
||||||
|
lastTickAt={lastTickAt}
|
||||||
|
>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<span className={styles.label}>Lat, Lon</span>
|
||||||
|
<span className={styles.value}>
|
||||||
|
{iss
|
||||||
|
? `${iss.latitude.toFixed(COORDS_DECIMALS)}°, ${iss.longitude.toFixed(COORDS_DECIMALS)}°`
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<span className={styles.label}>Alt</span>
|
||||||
|
<span className={styles.value}>
|
||||||
|
{iss ? `${iss.altitude.toFixed(ALT_DECIMALS)} km` : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<span className={styles.label}>Vel</span>
|
||||||
|
<span className={styles.value}>
|
||||||
|
{iss ? `${Math.round(iss.velocity).toLocaleString()} km/h` : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<span className={styles.label}>Next Pass</span>
|
||||||
|
<span className={styles.muted}>—</span>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ISSPanel.displayName = 'ISSPanel'
|
||||||
Loading…
Reference in New Issue