mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(mobile): polish round: night close-ups, bundled real recording, demo runbook
- Close-ups stay readable at any hour: night-side imagery is graded instead of black, and a watchdog drops to flat terrain when the keyless terrain host stalls. - A bundled real ADS-B recording (23 Oct 2026, 08:25-11:06Z; NYC, London, Gulf) gives the time machine ghosts to replay on first open. - Director beats frame their subjects (hearts rise over Midtown), fire a deterministic alert, and run in any order. - Dossiers name operators from airline prefixes and show the privacy notice only for real privacy flags. - The iPad Moment pane carries the record and chat; time machine labels no longer collide; demo data ships in the production build. - mobile/DEMO.md explains how to run and present the demo. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
8 files changed
+25828
-912
No files matched your search
@@ -0,0 +1,50 @@
|
|||||||
|
# Running the mobile demo
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:4173/mobile/` in Chrome. Open DevTools, turn on the device toolbar (Cmd+Shift+M), and pick a phone (iPhone 14 Pro Max is close to the 17 Pro Max at 430×932) or an iPad Pro (1024×1366). The two-pane tablet layout starts at 600 px wide.
|
||||||
|
|
||||||
|
No keys are needed. Load the page once before presenting: the first load compiles and caches, and later loads paint Today in under a second from the last good snapshot.
|
||||||
|
|
||||||
|
## Presenter keys
|
||||||
|
|
||||||
|
Press `.` to open the director. Press a key to jump to a beat. Each beat sets up its own state, so they can be run in any order.
|
||||||
|
|
||||||
|
| Key | Beat | What to say |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Today | The daily brief: since you last looked, today's Moment, your sky, your watch. |
|
||||||
|
| 2 | Your sky now | Everything above you this minute, from live ADS-B and orbital elements. |
|
||||||
|
| 3 | The Empire State Moment | A 4D reconstruction: helicopter tracks, sealed captures with view cones, chat and the record on one clock. Illustrative. |
|
||||||
|
| 4 | The Live lane, with hearts | Twitch-style chat over the city; reactions rise from where people are. Simulated chat. |
|
||||||
|
| 5 | Need eyes | A room asks for coverage; the first sealed answer earns the byline and Signal. |
|
||||||
|
| 6 | Capture | Look, Witness, Live and Upload, signed on the device. |
|
||||||
|
| 7 | Dossier of a live aircraft | Gotham-style: identity, live state, track, links, receipts. Real data. |
|
||||||
|
| 8 | Link graph | Everything that thing touches, synchronized with the globe. |
|
||||||
|
| 9 | Time machine at 60× | Replays a real bundled recording of London, NYC and the Gulf. |
|
||||||
|
| 0 | Clip this Moment | Select a range and clip it into a shareable Moment. |
|
||||||
|
| - | An alert, calmly | Severity-gated alert rules on live data. |
|
||||||
|
| = | Search to dossier | Search "7700" (or "ISS" if nothing is squawking) and open the result. |
|
||||||
|
|
||||||
|
`Esc` closes the top sheet. The director has a **Reset demo** action.
|
||||||
|
|
||||||
|
## What's real
|
||||||
|
|
||||||
|
| Real | Illustrative or simulated |
|
||||||
|
| --- | --- |
|
||||||
|
| Live aircraft, military, satellites, quakes, cyclones, fires, radar, lightning, wind, air quality, GNSS interference, transit, public cameras | The Empire State Moment (synthetic tracks over real NYC footprints) |
|
||||||
|
| Detections (circling, squawks, quake clusters) on live data | Chat in the Live lane, example posts, crews, and other contributors |
|
||||||
|
| The time machine's recording (this session's polling plus a bundled real recording from 23 Oct 2026, 08:25–11:06Z) | Need eyes requests from other people |
|
||||||
|
| Capture signing, hash chains and verification (WebCrypto on this device) | The room clerk (rules, not a model) |
|
||||||
|
|
||||||
|
Every illustrative or simulated surface carries a label in the app.
|
||||||
|
|
||||||
|
## Rehearse
|
||||||
|
|
||||||
|
Run the beats once on the presenting Mac before the meeting. On first load the keyless terrain host can be slow; if a close-up stays black for more than a few seconds, the engine falls back to flat terrain on its own.
|
||||||
|
|
||||||
|
Refresh the bundled data while `npm run dev` is running: `node mobile/demo/capture-snapshot.mjs --base http://localhost:4173` for the snapshot, and `node mobile/demo/capture-history.mjs --base http://localhost:4173 --minutes 150` for the recording (it writes every 5 minutes; behind a proxy, prefix it with `NODE_USE_ENV_PROXY=1`).
|
||||||
+5498
-98
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@
|
|||||||
"format": "gev-history-index/1",
|
"format": "gev-history-index/1",
|
||||||
"capturedAt": {
|
"capturedAt": {
|
||||||
"start": "2026-10-23T08:25:37.000Z",
|
"start": "2026-10-23T08:25:37.000Z",
|
||||||
"end": "2026-10-23T09:02:03.000Z"
|
"end": "2026-10-23T11:06:30.000Z"
|
||||||
},
|
},
|
||||||
"regions": [
|
"regions": [
|
||||||
{
|
{
|
||||||
@@ -10,21 +10,21 @@
|
|||||||
"label": "New York area",
|
"label": "New York area",
|
||||||
"bbox": { "west": -76.5, "south": 39.4, "east": -71.2, "north": 42.2 },
|
"bbox": { "west": -76.5, "south": 39.4, "east": -71.2, "north": 42.2 },
|
||||||
"file": "nyc.json",
|
"file": "nyc.json",
|
||||||
"tracks": 47
|
"tracks": 525
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "london",
|
"id": "london",
|
||||||
"label": "London and the North Sea approaches",
|
"label": "London and the North Sea approaches",
|
||||||
"bbox": { "west": -2.6, "south": 50.2, "east": 5.6, "north": 53.6 },
|
"bbox": { "west": -2.6, "south": 50.2, "east": 5.6, "north": 53.6 },
|
||||||
"file": "london.json",
|
"file": "london.json",
|
||||||
"tracks": 500
|
"tracks": 1559
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gulf",
|
"id": "gulf",
|
||||||
"label": "Gulf of Mexico",
|
"label": "Gulf of Mexico",
|
||||||
"bbox": { "west": -98.5, "south": 23.5, "east": -81.5, "north": 31.2 },
|
"bbox": { "west": -98.5, "south": 23.5, "east": -81.5, "north": 31.2 },
|
||||||
"file": "gulf.json",
|
"file": "gulf.json",
|
||||||
"tracks": 78
|
"tracks": 493
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+14043
-746
File diff suppressed because it is too large.
Load diff
+6217
-57
File diff suppressed because it is too large.
Load diff
@@ -42,7 +42,7 @@ const GRADE = Object.freeze({
|
|||||||
* while it is on screen (the keyless terrain host throttles under load, and
|
* while it is on screen (the keyless terrain host throttles under load, and
|
||||||
* Cesium then shows only the star field), fall back to the flat ellipsoid.
|
* Cesium then shows only the star field), fall back to the flat ellipsoid.
|
||||||
*/
|
*/
|
||||||
const TERRAIN_STALL_MS = 5000;
|
const TERRAIN_STALL_MS = 3000;
|
||||||
|
|
||||||
export const BASE_CREDITS = Object.freeze([
|
export const BASE_CREDITS = Object.freeze([
|
||||||
{
|
{
|
||||||
|
|||||||
+12
-5
@@ -171,6 +171,9 @@ function applyTab(tab) {
|
|||||||
if (tab === 'live' && two) chrome?.renderLeadView();
|
if (tab === 'live' && two) chrome?.renderLeadView();
|
||||||
if (tab !== 'live') chrome?.hidePick();
|
if (tab !== 'live') chrome?.hidePick();
|
||||||
}
|
}
|
||||||
|
// Paint the right panes in the first frame (on a tablet the Globe pane
|
||||||
|
// would otherwise cover Today until the engine is up).
|
||||||
|
applyTab(current);
|
||||||
|
|
||||||
function selectTab(tab) {
|
function selectTab(tab) {
|
||||||
if (tab === current) return;
|
if (tab === current) return;
|
||||||
@@ -967,12 +970,14 @@ async function openThreadFor(s) {
|
|||||||
/**
|
/**
|
||||||
* An oblique view of a place, framed into the part of the map a sheet leaves
|
* An oblique view of a place, framed into the part of the map a sheet leaves
|
||||||
* visible: on a phone the medium sheet covers the lower half, so the place
|
* visible: on a phone the medium sheet covers the lower half, so the place
|
||||||
* sits in the upper third; on iPad (sheets in the leading pane) a little
|
* sits in the upper third; on iPad (sheets in the leading pane) at the
|
||||||
* below the centre, leaving room above for rising reactions.
|
* centre, seen more steeply, leaving room above for rising reactions. The default
|
||||||
|
* heading looks north, up Midtown toward Central Park, which keeps
|
||||||
|
* the airports' dense live traffic (LaGuardia, JFK, Newark) out of frame.
|
||||||
*/
|
*/
|
||||||
function frameForSheet(
|
function frameForSheet(
|
||||||
{ lon, lat },
|
{ lon, lat },
|
||||||
{ distM = 3000, rayDeg = 30, heading = 32, duration = 1.6 } = {},
|
{ distM = 3000, rayDeg = 30, heading = 12, duration = 1.6 } = {},
|
||||||
) {
|
) {
|
||||||
if (!globe) return Promise.resolve();
|
if (!globe) return Promise.resolve();
|
||||||
const r = stage.getBoundingClientRect();
|
const r = stage.getBoundingClientRect();
|
||||||
@@ -980,7 +985,9 @@ function frameForSheet(
|
|||||||
const H = r.height || 1;
|
const H = r.height || 1;
|
||||||
const fov = globe.raw?.viewer?.camera?.frustum?.fov ?? Math.PI / 3;
|
const fov = globe.raw?.viewer?.camera?.frustum?.fov ?? Math.PI / 3;
|
||||||
const tanHalfV = W > H ? (Math.tan(fov / 2) * H) / W : Math.tan(fov / 2);
|
const tanHalfV = W > H ? (Math.tan(fov / 2) * H) / W : Math.tan(fov / 2);
|
||||||
const yFrac = isTwoPane() ? 0.56 : 0.27;
|
// On a tablet the whole stage is free: look steeper, place at centre.
|
||||||
|
if (isTwoPane()) rayDeg = Math.max(rayDeg, 42);
|
||||||
|
const yFrac = isTwoPane() ? 0.5 : 0.27;
|
||||||
const above = Math.atan((0.5 - yFrac) * 2 * tanHalfV) * (180 / Math.PI);
|
const above = Math.atan((0.5 - yFrac) * 2 * tanHalfV) * (180 / Math.PI);
|
||||||
const pitch = Math.max(-88, -rayDeg - above);
|
const pitch = Math.max(-88, -rayDeg - above);
|
||||||
const ray = (rayDeg * Math.PI) / 180;
|
const ray = (rayDeg * Math.PI) / 180;
|
||||||
@@ -1027,7 +1034,7 @@ async function busiestRecording(g) {
|
|||||||
|
|
||||||
/** Director: a steady stream of reactions rising from a place. */
|
/** Director: a steady stream of reactions rising from a place. */
|
||||||
let reactTimer = 0;
|
let reactTimer = 0;
|
||||||
function reactionStream(g, { lon, lat }, ms = 15000) {
|
function reactionStream(g, { lon, lat }, ms = 45000) {
|
||||||
clearInterval(reactTimer);
|
clearInterval(reactTimer);
|
||||||
const kinds = ['heart', 'heart', 'fire', 'heart', 'eyes', 'wow', 'heart'];
|
const kinds = ['heart', 'heart', 'fire', 'heart', 'eyes', 'wow', 'heart'];
|
||||||
const until = Date.now() + ms;
|
const until = Date.now() + ms;
|
||||||
|
|||||||
+3
-1
@@ -172,7 +172,9 @@ const PATTERNS = {
|
|||||||
export function haptic(kind = 'tick') {
|
export function haptic(kind = 'tick') {
|
||||||
if (!settings.haptics) return;
|
if (!settings.haptics) return;
|
||||||
try {
|
try {
|
||||||
if (navigator.vibrate) navigator.vibrate(PATTERNS[kind] ?? 5);
|
// Chrome logs an intervention for vibrate() before any user gesture.
|
||||||
|
if (navigator.vibrate && navigator.userActivation?.hasBeenActive !== false)
|
||||||
|
navigator.vibrate(PATTERNS[kind] ?? 5);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user