feat(mobile): rooms, live and upload capture, and the altitude-band identity

The Now feed mixes sensor, eye, track, number, witness and room cards
across Orbit, Sky, Ground and Inside, ordered so neighbours differ, with
composited thumbnails and live public-camera frames.

Every situation has a room: typed, place- and time-anchored posts,
evidence badges, bridging-rated notes and a cited Known / Disputed /
Unknown local synthesis, with cross-tab presence over BroadcastChannel.
Example posts are labelled as such.

Capture adds Live (10 s segments sealed into a hash chain, shown on the
globe as a view frustum) and Upload (EXIF placement, Unverified tier),
and an attach sheet that files any capture into a nearby situation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
Claude committed 2026-10-19 02:49:03 +00:00
1 parent ba1673a6db
commit 8b7fcb09df
38 files changed
+9837 -1081

No files matched your search

+657
View File
@@ -0,0 +1,657 @@
/**
* Offline stub situations, shaped like mobile/globe/API.md (band, archetype,
* telemetry, headline, media, series [{t,v}]). Everything here is labelled
* simulated; Eye cards point at the dev server's real public-camera frames
* (`/api/cctv/frame/:id`) and fall back to imagery when that is unreachable.
*/
import { esriThumb } from './dom.js';
export const SIM = 'Simulated · offline stub engine';
/** Public traffic cameras the dev server proxies (ids from /api/cctv/sources). */
export const STUB_CAMS = [
{
id: '354',
title: '5th St / Congress Ave',
city: 'Austin',
provider: 'Austin Transportation & Public Works',
lat: 30.267119,
lon: -97.743172,
headingDeg: 67.5,
},
{
id: 'tfl-00001.06502',
title: 'Trafalgar Square',
city: 'London',
provider: 'Transport for London',
lat: 51.5074,
lon: -0.12765,
headingDeg: 135,
},
{
id: 'wsdot-9520',
title: 'I-5 at 5th Ave / Columbia',
city: 'Seattle',
provider: 'WSDOT',
lat: 47.605166,
lon: -122.330316,
headingDeg: 202.5,
},
{
id: 'ca-d4-tv301',
title: 'US-101 at Octavia St',
city: 'San Francisco',
provider: 'Caltrans',
lat: 37.77146,
lon: -122.42323,
headingDeg: 180,
},
{
id: 'no-vegvesen-3000051_1',
title: 'F862 Sentrumstangenten',
city: 'Tromsø',
provider: 'Statens vegvesen',
lat: 69.64469,
lon: 18.947695,
headingDeg: 270,
},
{
id: 'ca-d11-x1248',
title: 'I-5 at First Avenue',
city: 'San Diego',
provider: 'Caltrans',
lat: 32.72285,
lon: -117.16356,
headingDeg: 180,
},
{
id: 'fi-c1455101',
title: 'vt4 Rovaniemi Revontuli',
city: 'Rovaniemi',
provider: 'Fintraffic',
lat: 66.503858,
lon: 25.725196,
headingDeg: 202.5,
},
];
export const camFrameUrl = (id) =>
`/api/cctv/frame/${encodeURIComponent(id)}?ts=${Math.floor(Date.now() / 10000)}`;
function series(values, stepMin, now) {
return values.map((v, i) => ({
t: now - (values.length - 1 - i) * stepMin * 60e3,
v,
}));
}
export function buildStubSituations(now) {
const min = 60e3;
const S = (o) => ({
live: true,
source: SIM,
section: 'now',
severity: 0,
updatedAt: now - (o.age ?? 2) * min,
time: o.time ?? { start: now - 6 * 3600e3, end: now + 24 * 3600e3 },
camera: o.camera ?? {
lon: o.lon,
lat: o.lat,
height: o.height ?? 400e3,
heading: 0,
pitch: -90,
},
thumbnail: esriThumb(o.lon, o.lat, o.span ?? 1, 480, o.th ?? 600),
...o,
});
const eye = (cam, o) =>
S({
kind: 'camera',
family: 'eyes',
band: 'ground',
archetype: 'eye',
lon: cam.lon,
lat: cam.lat,
span: 0.01,
height: 1500,
camera: {
lon: cam.lon,
lat: cam.lat,
height: 900,
heading: cam.headingDeg,
pitch: -30,
},
layers: ['eyes.public-cctv', 'ground.traffic-flow'],
media: {
kind: 'image',
url: camFrameUrl(cam.id),
refreshMs: 15000,
cameraId: cam.id,
headingDeg: cam.headingDeg,
provider: cam.provider,
city: cam.city,
},
telemetry: `GROUND · CAM ${cam.id.slice(-6).toUpperCase()} · ${cam.title.toUpperCase()}`,
source: `${cam.provider} public camera`,
...o,
});
const C = Object.fromEntries(STUB_CAMS.map((c) => [c.id, c]));
return [
S({
id: 'cyclone-wpac',
kind: 'cyclone',
family: 'hazards',
band: 'orbit',
archetype: 'sensor',
severity: 5,
title: 'Typhoon tracking toward Luzon',
subtitle: 'Category 4 · eyewall replacement under way',
place: 'Philippine Sea',
lon: 126.5,
lat: 15.4,
span: 9,
th: 640,
height: 2200e3,
layers: ['hazards.tropical-cyclones', 'weather.clouds', 'weather.wind'],
metrics: [
{ label: 'Wind', value: 185, unit: 'km/h' },
{ label: 'Pressure', value: 935, unit: 'hPa' },
{ label: 'Moving', value: 'WNW 18', unit: 'km/h' },
],
headline: { value: 185, unit: 'km/h', label: 'sustained wind' },
telemetry: 'ORBIT · GOES IR · 185 KM/H · 935 HPA · WNW 18',
series: series(
[120, 130, 140, 150, 165, 175, 185, 185, 180, 185],
360,
now,
),
seriesLabel: 'sustained wind, km/h',
age: 2,
time: { start: now - 72 * 3600e3, end: now + 120 * 3600e3 },
events: [{ at: now + 30 * 3600e3, label: 'Landfall (fcst)' }],
}),
eye(C['354'], {
id: 'eye-austin',
title: 'Saturday night on Congress Ave',
subtitle: 'Downtown crowds after the game',
place: 'Downtown Austin, Texas',
severity: 2,
age: 0,
}),
S({
id: 'squawk-7700',
kind: 'squawk',
family: 'sky',
band: 'sky',
archetype: 'track',
severity: 4,
title: 'Squawk 7700 · descending toward Halifax',
subtitle: 'Simulated emergency track, diverting',
place: 'Over Nova Scotia',
lon: -62.4,
lat: 45.1,
span: 2.4,
th: 520,
height: 350e3,
layers: ['sky.emergency-squawks', 'sky.commercial-flights'],
metrics: [
{ label: 'Altitude', value: 'FL100', unit: '' },
{ label: 'Speed', value: 312, unit: 'kt' },
{ label: 'Squawk', value: '7700', unit: '' },
],
headline: { value: '7700', unit: '', label: 'general emergency' },
telemetry: 'SKY · FL100 ↓ · 312 KT · SQ 7700',
track: { headingDeg: 310 },
age: 1,
}),
S({
id: 'lightning-gulf',
kind: 'lightning',
family: 'weather',
band: 'sky',
archetype: 'number',
severity: 3,
title: 'Lightning over the Gulf of Mexico',
subtitle: 'A squall line pushing toward the Louisiana coast',
place: 'Gulf of Mexico',
lon: -90.2,
lat: 27.4,
span: 5,
th: 460,
height: 900e3,
layers: ['weather.lightning', 'weather.radar'],
metrics: [
{ label: 'Strikes / h', value: 4812, unit: '' },
{ label: 'Peak density', value: 38, unit: '/km²' },
],
headline: { value: 4812, unit: '', label: 'strikes in the last hour' },
telemetry: 'SKY · GLM · 4,812 STR/H · 27.4N 90.2W',
series: series([620, 900, 1400, 2100, 2900, 3600, 4100, 4812], 15, now),
seriesLabel: 'strikes / 15 min',
age: 1,
}),
S({
id: 'quake-24h',
kind: 'earthquake',
family: 'hazards',
band: 'ground',
archetype: 'sensor',
severity: 4,
title: 'M6.4 · largest quake in 24 h',
subtitle: 'Shallow thrust event, aftershocks continuing',
place: 'Off Hokkaido, Japan',
lon: 144.2,
lat: 42.1,
span: 3.5,
th: 480,
height: 900e3,
layers: ['hazards.earthquakes'],
metrics: [
{ label: 'Magnitude', value: 6.4, unit: 'Mw' },
{ label: 'Depth', value: 38, unit: 'km' },
{ label: 'Aftershocks', value: 27, unit: '' },
],
headline: { value: 6.4, unit: 'Mw', label: 'magnitude' },
telemetry: 'GROUND · USGS · M6.4 · 38 KM DEEP',
age: 11,
events: [{ at: now - 14 * 3600e3, label: 'M6.4' }],
}),
S({
id: 'iss-pass',
kind: 'iss',
family: 'orbit',
band: 'orbit',
archetype: 'track',
severity: 2,
title: 'ISS crosses your sky at 21:42',
subtitle: 'Bright pass · max elevation 71°',
place: 'Low Earth orbit',
lon: -40,
lat: 30,
span: 30,
th: 520,
height: 9000e3,
layers: ['orbit.iss-crewed', 'orbit.ground-tracks'],
metrics: [
{ label: 'Altitude', value: 418, unit: 'km' },
{ label: 'Speed', value: 7.66, unit: 'km/s' },
{ label: 'Next pass', value: '21:42', unit: '' },
],
headline: { value: '21:42', unit: '', label: 'next pass · 71° max' },
telemetry: 'ORBIT · 418 KM · 7.66 KM/S · PASS 21:42',
track: { headingDeg: 52 },
age: 1,
}),
S({
id: 'fires-sierra',
kind: 'fires',
family: 'hazards',
band: 'ground',
archetype: 'sensor',
severity: 4,
title: 'Fire cluster in the Sierra foothills',
subtitle: 'Hotspots doubling since dawn, smoke drifting east',
place: 'Nevada County, California',
lon: -120.9,
lat: 39.05,
span: 1.6,
th: 560,
height: 260e3,
layers: [
'hazards.active-fires',
'hazards.fire-perimeters',
'weather.wind',
],
metrics: [
{ label: 'Hotspots', value: 214, unit: '' },
{ label: 'FRP', value: 1840, unit: 'MW' },
{ label: 'Wind', value: 32, unit: 'km/h' },
],
headline: { value: 214, unit: '', label: 'hotspots' },
telemetry: 'GROUND · FIRMS VIIRS · 214 HOTSPOTS · WIND W 32',
series: series([12, 18, 30, 44, 61, 88, 120, 160, 190, 214], 60, now),
seriesLabel: 'hotspots / h',
age: 4,
}),
S({
id: 'gnss-baltic',
kind: 'gnss',
family: 'infra',
band: 'sky',
archetype: 'number',
severity: 3,
title: 'GNSS jamming over the Baltic',
subtitle: 'Aircraft reporting degraded navigation integrity',
place: 'Baltic Sea',
lon: 21.0,
lat: 56.5,
span: 6,
th: 480,
height: 1500e3,
layers: ['infra.gnss-interference', 'sky.commercial-flights'],
metrics: [
{ label: 'Affected', value: 312, unit: 'aircraft' },
{ label: 'Area', value: 84, unit: 'k km²' },
],
headline: { value: 312, unit: '', label: 'aircraft affected' },
telemetry: 'SKY · ADS-B NIC<7 · 312 AC · 84K KM²',
series: series([120, 180, 240, 260, 300, 312], 60, now),
seriesLabel: 'aircraft with NIC < 7',
age: 9,
}),
eye(C['tfl-00001.06502'], {
id: 'eye-trafalgar',
title: 'Trafalgar Square, live',
subtitle: 'Looking south-east down Whitehall',
place: 'Westminster, London',
severity: 1,
age: 0,
}),
S({
id: 'novosel',
kind: 'helicopters',
family: 'sky',
band: 'sky',
archetype: 'track',
title: 'Rotor school: 14th circuit of the day',
subtitle: 'A training helicopter flying the pattern',
place: 'Fort Novosel, Alabama',
lon: -85.72,
lat: 31.36,
span: 0.22,
th: 420,
height: 40e3,
layers: ['sky.helicopters'],
metrics: [
{ label: 'Airborne', value: 23, unit: 'rotorcraft' },
{ label: 'Pattern alt', value: 700, unit: 'ft' },
],
headline: { value: 14, unit: 'th', label: 'circuit today' },
telemetry: 'SKY · 700 FT · 88 KT · CIRCUIT 14',
track: { headingDeg: 140, circuit: true },
age: 3,
alwaysOn: true,
}),
S({
id: 'bhote-koshi-2026',
kind: 'reconstruction',
family: 'hazards',
band: 'inside',
archetype: 'sensor',
title: 'Bhote Koshi outburst flood',
subtitle: '16 sourced observations · 3 open hypotheses',
place: 'Rasuwa, Nepal ↔ Gyirong, Tibet',
section: 'reconstructions',
lon: 85.33,
lat: 28.15,
span: 0.3,
th: 640,
height: 30e3,
camera: {
lon: 85.3548,
lat: 28.207,
height: 14e3,
heading: 344,
pitch: -39,
},
layers: [
'hazards.floods',
'infra.recent-imagery',
'eyes.signed-captures',
],
metrics: [
{ label: 'Observations', value: 16, unit: '' },
{ label: 'Corridor', value: 145, unit: 'pts' },
{ label: 'Hypotheses', value: 3, unit: '' },
],
telemetry: 'INSIDE · 4D · 16 OBS · 22 KM CORRIDOR',
live: false,
age: 60 * 24 * 7,
source: 'GeoPera reconstruction · Vantor Open Data (CC BY-NC 4.0)',
time: { start: Date.UTC(2026, 7, 26, 0), end: Date.UTC(2026, 7, 27, 12) },
thread: 'bhote-koshi-2026',
thumbnail: '/events/bhote-koshi-2026/post.webp',
}),
S({
id: 'rain-houston',
kind: 'rain',
family: 'weather',
band: 'sky',
archetype: 'sensor',
severity: 3,
title: 'Heavy rain cell over Houston',
subtitle: '60 dBZ core over the Ship Channel, moving east',
place: 'Houston, Texas',
lon: -95.25,
lat: 29.74,
span: 0.9,
th: 540,
height: 120e3,
layers: ['weather.radar', 'weather.lightning'],
metrics: [
{ label: 'Peak', value: 60, unit: 'dBZ' },
{ label: 'Rate', value: 76, unit: 'mm/h' },
],
headline: { value: 76, unit: 'mm/h', label: 'rain rate' },
telemetry: 'SKY · MRMS · 60 DBZ · 76 MM/H · E 22 KM/H',
age: 2,
}),
S({
id: 'starbase',
kind: 'launch',
family: 'orbit',
band: 'orbit',
archetype: 'track',
title: 'Launch window opens at Starbase',
subtitle: 'Trajectory east over the Gulf',
place: 'Boca Chica, Texas',
lon: -97.157,
lat: 25.997,
span: 0.06,
th: 380,
height: 12e3,
layers: ['orbit.launches', 'orbit.ground-tracks'],
metrics: [
{ label: 'T−', value: '06:12:00', unit: '' },
{ label: 'Weather go', value: 80, unit: '%' },
],
headline: { value: 'T−06:12', unit: '', label: 'to window' },
telemetry: 'ORBIT · PAD B · AZ 097° · T−06:12:00',
track: { headingDeg: 97, launch: true },
age: 6,
events: [{ at: now + 6.2 * 3600e3, label: 'T-0' }],
}),
eye(C['wsdot-9520'], {
id: 'eye-seattle',
title: 'I-5 through downtown Seattle',
subtitle: 'Southbound into the convention center tunnel',
place: 'Seattle, Washington',
age: 0,
}),
S({
id: 'suez-queue',
kind: 'queue',
family: 'sea',
band: 'ground',
archetype: 'number',
title: '41 ships waiting at Suez',
subtitle: 'Southbound convoy holding in the anchorage',
place: 'Gulf of Suez, Egypt',
lon: 32.55,
lat: 29.88,
span: 0.35,
th: 560,
height: 60e3,
layers: ['sea.ais-vessels', 'sea.tankers-cargo'],
metrics: [
{ label: 'Waiting', value: 41, unit: 'vessels' },
{ label: 'Median wait', value: 19, unit: 'h' },
],
headline: { value: 41, unit: '', label: 'vessels at anchor' },
telemetry: 'GROUND · AIS · 41 AT ANCHOR · WAIT 19 H',
series: series([28, 30, 33, 35, 39, 41], 120, now),
seriesLabel: 'vessels at anchor',
age: 5,
}),
S({
id: 'north-sea-mil',
kind: 'military',
family: 'sky',
band: 'sky',
archetype: 'sensor',
title: 'Tanker orbits over the North Sea',
subtitle: 'Two refuelling tracks and a fighter CAP',
place: 'North Sea',
lon: 3.5,
lat: 55.5,
span: 5,
th: 520,
height: 900e3,
layers: ['sky.military-aircraft'],
metrics: [
{ label: 'Aircraft', value: 14, unit: '' },
{ label: 'Tankers', value: 3, unit: '' },
],
telemetry: 'SKY · FL260 · 3 TANKERS · 14 MIL',
age: 7,
}),
eye(C['no-vegvesen-3000051_1'], {
id: 'eye-tromso',
kind: 'camera',
title: 'Tromsø under a Kp 6 sky',
subtitle: 'Road camera on Sentrumstangenten, aurora nowcast high',
place: 'Tromsø, Norway',
age: 0,
}),
S({
id: 'jet-stream',
kind: 'jet',
family: 'weather',
band: 'sky',
archetype: 'sensor',
title: 'Jet stream at 210 kt over the Atlantic',
subtitle: 'Eastbound flights riding it, westbound avoiding it',
place: 'North Atlantic',
lon: -38,
lat: 49,
span: 22,
th: 460,
height: 4500e3,
layers: ['weather.wind', 'sky.commercial-flights'],
metrics: [{ label: 'Core', value: 210, unit: 'kt' }],
headline: { value: 210, unit: 'kt', label: 'jet core at FL340' },
telemetry: 'SKY · GFS 250 HPA · 210 KT · FL340',
age: 20,
}),
S({
id: 'atl-rush',
kind: 'airport',
family: 'sky',
band: 'sky',
archetype: 'number',
title: 'Busiest airport right now',
subtitle: 'Hartsfield–Jackson arrival bank',
place: 'Atlanta, Georgia',
lon: -84.428,
lat: 33.6407,
span: 0.09,
th: 600,
height: 18e3,
layers: [
'sky.commercial-flights',
'sky.holding-patterns',
'sky.airport-boards',
],
metrics: [
{ label: 'Movements', value: 182, unit: '/h' },
{ label: 'Holding', value: 6, unit: '' },
],
headline: { value: 182, unit: '/h', label: 'movements' },
telemetry: 'SKY · KATL · 182 MVT/H · 6 HOLDING',
series: series([120, 140, 166, 170, 182, 176, 181, 182], 30, now),
seriesLabel: 'movements / h',
age: 1,
alwaysOn: true,
}),
eye(C['ca-d4-tv301'], {
id: 'eye-sf',
title: 'US-101 at Octavia, San Francisco',
subtitle: 'Central Freeway merging into Mission',
place: 'San Francisco, California',
age: 0,
alwaysOn: true,
}),
S({
id: 'tokyo-rush',
kind: 'transit',
family: 'ground',
band: 'ground',
archetype: 'sensor',
title: 'Rush hour on the Yamanote line',
subtitle: 'Headways at 2 min 30 s through Shinjuku',
place: 'Shinjuku, Tokyo',
lon: 139.7,
lat: 35.69,
span: 0.05,
th: 600,
height: 9e3,
layers: ['ground.transit-vehicles', 'ground.rail'],
metrics: [
{ label: 'Trains moving', value: 1840, unit: '' },
{ label: 'On time', value: 98.6, unit: '%' },
],
telemetry: 'GROUND · GTFS-RT · 1,840 TRAINS · 98.6% OT',
age: 2,
alwaysOn: true,
}),
S({
id: 'aurora',
kind: 'aurora',
family: 'weather',
band: 'orbit',
archetype: 'sensor',
title: 'Aurora oval reaching 60°N tonight',
subtitle: 'Kp 6 storm, clear skies after 22:00',
place: 'Northern Scandinavia',
lon: 18.95,
lat: 69.65,
span: 8,
th: 600,
height: 1200e3,
layers: ['weather.clouds'],
metrics: [
{ label: 'Kp', value: 6, unit: '' },
{ label: 'Cloud', value: 20, unit: '%' },
],
headline: { value: 'Kp 6', unit: '', label: 'geomagnetic storm' },
telemetry: 'ORBIT · OVATION · KP 6 · OVAL 60N',
series: series([2, 3, 3, 4, 5, 6, 6], 180, now),
seriesLabel: 'Kp index',
age: 15,
}),
S({
id: 'singapore',
kind: 'strait',
family: 'sea',
band: 'ground',
archetype: 'sensor',
title: 'Singapore Strait at capacity',
subtitle: 'Eastbound lane full, 318 ships at anchor',
place: 'Singapore Strait',
lon: 103.85,
lat: 1.22,
span: 0.4,
th: 420,
height: 70e3,
layers: ['sea.ais-vessels'],
metrics: [
{ label: 'Transits', value: 22, unit: '/h' },
{ label: 'At anchor', value: 318, unit: '' },
],
telemetry: 'GROUND · AIS · 22 TRANSITS/H · 318 ANCHORED',
age: 3,
alwaysOn: true,
}),
];
}