🔍 crossover post-merge review — 18 confirmed findings fixed (fable)

A fresh-eyes adversarial pass over the 15 pulled commits (godrum +
keyroll + sats + planes cache + god's eye): 21 raised, 18 confirmed,
all fixed.

The instrument: .mid export no longer smuggles the seeded demo beat
(lanes export only if playing or edited off the seed — same test
zeroHasBuild uses); master ▶/■ remembers per-lane mutes; the beat
panel's bpm field is always editable like the dial (🔒 blocks the
world's hands, not the user's); ⏺ rec with the beat stopped anchors
step 0 at arm instead of the invisible free-running clock; note-length
drags speak pointer events (touch works, window-level so mid-drag
re-renders survive); the resize handle no longer eats 40% of a
sixteenth's click width; the god's eye clears a stale ISS trail after
an rAF nap or resize; the "Agent A" dev-process string is gone; the
grimoire now tells the truth about kit knobs and which voices hold
note length (manual regenerated).

The workers: world_sats — partial Celestrak fetches backfill from the
old cache instead of clobbering it, pass-detection state survives the
6-hourly refresh, the no-TLE exit naps 15 min so the supervisor can't
hammer Celestrak, and the venue default now matches world_sky's
Brisbane instead of Null Island; world_planes bounds its stale-cache
fallback to 1h so a days-old sky can't resurrect as live; world_sky's
docstring admits the lat/lon it emits. run.py retires workers that
exit 0 instead of reviving them every 10s (the sgp4-missing churn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-13 21:24:25 +10:00
parent d2c7938321
commit 629cbad6fd
6 changed files with 80 additions and 33 deletions

5
run.py
View File

@ -198,6 +198,11 @@ def main():
now = time.time()
for w in workers: # revive any worker that died
if w["proc"].poll() is not None and now >= w["next_ok"]:
if w["proc"].returncode == 0: # exit 0 = a deliberate, polite goodbye
if not w.get("retired"): # (missing dep etc.) — don't churn it
w["retired"] = True
print(f" ({os.path.basename(w['path'])} exited cleanly; retired)")
continue
print(f" ({os.path.basename(w['path'])} died; restarting)")
np = launch(w["path"], w["extra"])
if np:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -172,14 +172,14 @@ def main():
print(f"[{NAME}] HTTP 429 rate limited; backing off to {poll:.0f}s")
else:
print(f"[{NAME}] HTTP {ex.code}; retrying in {poll:.0f}s")
_stale = read_cache(None) # 429 -> a stale cache beats nothing
if _stale is not None:
_stale = read_cache(3600) # 429 -> a stale cache beats nothing,
if _stale is not None: # but a day-old sky beats neither
last = reduce_states(filter_states_bbox(_stale[0], args.bbox))
print(f"[{NAME}] serving stale cache ({_stale[1]:.0f}s old)")
except (urllib.error.URLError, TimeoutError, OSError,
json.JSONDecodeError, ValueError) as ex:
print(f"[{NAME}] fetch error: {ex}; retrying in {poll:.0f}s")
_stale = read_cache(None) # network down -> fall back to a stale cache
_stale = read_cache(3600) # network down -> fall back, bounded 1h
if _stale is not None:
last = reduce_states(filter_states_bbox(_stale[0], args.bbox))
print(f"[{NAME}] serving stale cache ({_stale[1]:.0f}s old)")

View File

@ -142,11 +142,13 @@ def load_grouped_tles(force_fetch=False):
print(f"[{NAME}] using cached TLEs (< 24 h old)")
return groups
merged, recon_catnrs = [], set()
any_failed = False
for url, is_recon in TLE_SOURCES:
try:
txt = fetch_url(url)
except (urllib.error.URLError, OSError, ValueError) as e:
print(f"[{NAME}] TLE fetch failed for {url.split('?')[-1]}: {e}")
any_failed = True
continue
for name, l1, l2 in parse_tles(txt):
catnr = l1[2:7].strip()
@ -154,6 +156,14 @@ def load_grouped_tles(force_fetch=False):
recon_catnrs.add(catnr)
merged.append((name, l1, l2, is_recon))
if merged:
if any_failed: # partial fetch: backfill the failed groups
have = {l1[2:7].strip() for _n, l1, _l2, _r in merged} # from the old cache so a
old, _old_recon = _read_cache() # half-set never clobbers it
for name, l1, l2, is_recon in old:
if l1[2:7].strip() not in have:
merged.append((name, l1, l2, is_recon))
if is_recon:
recon_catnrs.add(l1[2:7].strip())
_write_cache(merged, recon_catnrs)
return merged
groups, _ = _read_cache() # network down — serve stale
@ -247,8 +257,8 @@ def main():
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--interval", type=float, default=2.0)
ap.add_argument("--lat", type=float, default=0.0, help="venue latitude (run.py retargets this)")
ap.add_argument("--lon", type=float, default=0.0, help="venue longitude")
ap.add_argument("--lat", type=float, default=-27.47, help="venue latitude (run.py retargets this; default matches world_sky's Brisbane)")
ap.add_argument("--lon", type=float, default=153.02, help="venue longitude")
args = ap.parse_args()
if not HAVE_SGP4:
@ -257,8 +267,12 @@ def main():
sats = build_sats(load_grouped_tles())
if not sats:
print(f"[{NAME}] no TLEs (no cache and no network) — exiting politely.")
sys.exit(0)
# nap before exiting nonzero: run.py revives dead workers ~10s later, and
# without this pause a downed network would mean a Celestrak attempt every
# revive. 15 min keeps us polite and self-healing.
print(f"[{NAME}] no TLEs (no cache and no network) — napping 15 min, then retrying via the supervisor.")
time.sleep(900)
sys.exit(1)
client = SimpleUDPClient(args.host, args.port)
n_recon = sum(1 for s in sats if s.is_recon)
@ -272,6 +286,9 @@ def main():
last_refresh = time.time()
fresh = build_sats(load_grouped_tles()) # refetches only if the cache is stale
if fresh:
prev = {s.catnr: s.prev_elev for s in sats} # carry pass-detection state so a
for s in fresh: # refresh mid-pass can't eat the event
s.prev_elev = prev.get(s.catnr)
sats = fresh
have_recon = any(s.is_recon for s in sats)

View File

@ -11,6 +11,8 @@ Emits:
/gs/sky/elev solar elevation in degrees (-90 night .. +90 overhead)
/gs/sky/day 0..1 smooth day/night (0 deep night, 1 broad daylight)
/gs/sky/az solar azimuth in degrees (0=N, 90=E, 180=S, 270=W)
/gs/sky/lat the venue's own latitude (so the console can place
/gs/sky/lon the venue's own longitude "here" on the god's eye map)
"""
from __future__ import annotations
@ -69,7 +71,7 @@ def main():
args = ap.parse_args()
client = SimpleUDPClient(args.host, args.port)
print(f"[sky] emitting /gs/sky/elev|day|az for ({args.lat},{args.lon}) "
print(f"[sky] emitting /gs/sky/elev|day|az|lat|lon for ({args.lat},{args.lon}) "
f"every {args.interval:g}s")
while True: