Skip to content

Tier 1 data enrichment: catalogue, brightness, rich detail panel - #7

Merged
Darkflib merged 4 commits into
mainfrom
claude/data-enrichment-caching-db3a40
Aug 1, 2026
Merged

Tier 1 data enrichment: catalogue, brightness, rich detail panel#7
Darkflib merged 4 commits into
mainfrom
claude/data-enrichment-caching-db3a40

Conversation

@Darkflib

@Darkflib Darkflib commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Adds a satellite enrichment catalogue — richer per-object data than orbital position alone — served same-origin and joined lazily by NORAD id, plus the Tier 1 UI to browse and read it.

What's here

Build pipeline (scripts/enrich/, Node, zero deps)

  • SATCAT (CelesTrak CSV) is the load-bearing base set; GCAT (McDowell TSV) adds mass/dimensions/shape/orbit-class/operator/status; mmccants qs.mag adds intrinsic magnitude (vendored — upstream qsmag.zip 404s; recovered from Wayback; freeware licence).
  • Per-field precedence with _sources provenance stamping; rolling 12-month decay window.
  • GCAT joins on its Satcat=NORAD column (98.3% join); emits a lean index, NORAD/1000 buckets, a manifest, and a generated SOURCES.md.
  • Fail-safe validation gate aborts before deploy on broken input, so a bad upstream never overwrites good published data.

Per-constellation magnitude fallback (constellations.mjs)

  • mmccants measures ~none of the big constellations, so a representative value per bus fills the gap — applied only when unmeasured and always labelled estimated (~ prefix, dashed badge, "Est. magnitude", estimate in sources).
  • Starlink is launch-date-split for the post-VisorSat/dielectric brightness drop: pre-2020-06 sats ~4.5, coated ~5.5.
  • Brightness coverage: 3,015 measured → ~15,857 with a signal.

Client (Tier 1 UI)

  • Selection panel gains a Catalogue section, a brightness badge, and a source line.
  • New Catalogue browser (third topbar tab): search + type/brightness filters over ~36k objects, click-through detail, and Show in 3D for loaded objects.

Deploy

  • enrich.yml rebuilds daily and redeploys fresh data, sharing static.yml's pages concurrency group; committed data/ is the seed. Bumped static.yml checkout@v4 → v6.

Also in this PR

  • Fix: reentry-mode panel collapse left the watch list visible (CSS specificity — an id-bearing mode rule outranked the generic collapse rule).

Verified

First real build: 36,174 records, GCAT 98.3%, magnitudes 3,015 (+12,842 estimated); index 4.8 MB → 276 KB gzip. Both UI surfaces verified in-browser (ISS full record incl. mag −2.5 "Naked eye"; Starlink degrades cleanly / shows estimated badge).

Notes for review

  • docs/data-enrichment-schema.md and docs/enrichment-build-job.md are the design; WORKLOG.md records findings and decisions made during the build (GCAT join key, the ISS decay-date bug, magnitude sourcing).
  • Observed but not changed (out of scope): the GP loader has no per-fetch timeout — a stalled CelesTrak group can hang the loading screen. Pre-existing; worth an AbortController later.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a searchable satellite catalogue with object-type and brightness filters.
    • Added detailed catalogue information for selected satellites, including launch, ownership, status, orbital, physical and source data.
    • Added brightness classifications and estimated magnitudes where measurements are unavailable.
    • Added navigation from catalogue results to objects displayed in the 3D scene.
    • Enrichment data loads as needed for faster browsing.
    • Added automated daily data refreshes for the published catalogue.
  • Documentation

    • Added information about catalogue sources, attribution and enrichment data.

Darkflib and others added 3 commits August 1, 2026 11:35
Build an enrichment pipeline (CelesTrak SATCAT + GCAT + McCants qs.mag)
that produces a same-origin, lazily-joined catalogue, and surface it in the
app as a rich detail panel, a brightness badge, and a new Catalogue browser.

Pipeline (scripts/enrich/, Node, no deps):
- SATCAT is the load-bearing base set; GCAT adds mass/dims/shape/orbit/
  operator/status; mmccants adds intrinsic magnitude (vendored — upstream
  qsmag.zip 404s; freeware licence).
- Per-field precedence with _sources provenance stamping; 12-month decay
  window (SATCAT DECAY_DATE only — GCAT DDate also marks assembly/renaming).
- GCAT joins on its Satcat=NORAD column (98.3% join); outputs a lean index
  plus NORAD/1000 buckets, a manifest, and generated SOURCES.md.
- Fail-safe validation gate aborts before deploy on broken input.

Client:
- src/enrichment.js: lazy bucket fetch + brightness bucketing.
- Selection panel gains a Catalogue section, brightness badge, source line.
- New Catalogue tab: search + type/brightness filters over ~36k objects,
  detail view, and Show-in-3D for loaded objects.

Deploy: enrich.yml rebuilds daily and redeploys fresh data, sharing
static.yml's pages concurrency group; committed data/ is the seed. Bump
static.yml checkout v4 -> v6.

See WORKLOG.md and docs/ for findings and open questions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The collapse button hid .panel-body/.panel-section generically, but reentry
mode force-shows #reentry-section via an id-bearing rule
(body.mode-reentry #reentry-section) that outranks it — so collapsing only
hid the id-less "Find a satellite" section. Add a matching-specificity rule
so the watch list collapses too, leaving just the header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mmccants measures almost none of the large constellations (0 Starlink/OneWeb
magnitudes in qs.mag), leaving the brightness badge blank for most of what
users click. Add a fallback that assigns one representative magnitude per
constellation bus, applied ONLY when there is no measured value.

Estimates are always surfaced as estimates: magSource 'estimate', magBasis
naming the constellation, a "~" prefix and dashed badge, an "Est. magnitude"
readout row, and "estimate" in the sources line.

Starlink is launch-date-split to reflect the brightness drop after the
mid-2020 VisorSat / dielectric coatings: pre-visor v0.9/v1.0 (before 2020-06)
get ~4.5, coated sats ~5.5. Covers 12,842 objects (11,381 Starlink); 179
early pre-visor Starlink remain and get the brighter estimate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46ea357c-5ca4-4b8e-949b-64b7343786cc

📥 Commits

Reviewing files that changed from the base of the PR and between 256e177 and 861d7f8.

📒 Files selected for processing (2)
  • scripts/enrich/sources/gcat.mjs
  • src/main.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/enrich/sources/gcat.mjs
  • src/main.js

Walkthrough

Adds a Node-based enrichment pipeline that combines satellite sources, writes NORAD-bucketed data, and deploys the site through GitHub Pages. Adds lazy catalogue loading, brightness classification, catalogue search, filters, object details, source attribution, documentation, and generated enrichment records.

Changes

Enrichment catalogue

Layer / File(s) Summary
Source adapters and enrichment contracts
scripts/enrich/sources/*, scripts/enrich/http.mjs, scripts/enrich/constellations.mjs, scripts/enrich/vendor/qs.mag, docs/*
Adds SATCAT, GCAT, and mmccants ingestion, conditional HTTP caching, magnitude estimation, source metadata, and enrichment design specifications.
Merge, validation, and generated outputs
scripts/enrich/build.mjs, scripts/enrich/merge.mjs, scripts/enrich/validate.mjs, scripts/enrich/write.mjs, data/enrichment/*, data/manifest.json, SOURCES.md, .gitignore
Merges source records with precedence and decay filtering, validates output counts, and writes deterministic catalogue indexes, enrichment buckets, manifests, and attribution data.
Client enrichment and catalogue browsing
src/enrichment.js, src/main.js, index.html, styles/main.css
Adds lazy record loading, brightness classes, selected-object enrichment, catalogue search and filtering, detail views, 3D navigation, and responsive styling.
Scheduled build and Pages deployment
.github/workflows/enrich.yml, .github/workflows/static.yml
Adds scheduled and manual enrichment builds, best-effort history updates, full-tree Pages deployment, and the checkout action update.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

Satellites gather in buckets of light,
Sources align through the build each night.
Shards load softly when objects are seen,
Brightness badges glow on the catalogue screen.
Pages deploys the orbital stream.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main changes: Tier 1 data enrichment, the catalogue, brightness information, and the rich detail panel.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/data-enrichment-caching-db3a40

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 256e177d13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!s) return null;
const t = s.trim().replace(/[?~*]/g, '');
if (!t || t === '-') return null;
const v = parseFloat(t);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat zero-valued GCAT physical fields as missing

When GCAT uses 0 for an unknown physical measurement, num() accepts it as real and the nullish fallback also stops at that zero. In the committed catalogue this produces 12,635 objects with a displayed mass of 0 kg and 12,417 with dimensions such as L 0 m · Ø 0 m · span 0 m, rather than omitting unavailable values; it can also prevent a useful Mass or DryMass value from being selected when TotMass is zero.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 861d7f8. Added a positive-only posNum() and use it for mass/length/diameter/span, so GCAT's 0 sentinel is treated as missing — no more 0 kg/Ø 0 m, and the ?? fallback now falls through a zero TotMass to a real Mass/DryMass.


Generated by Claude Code

Comment thread src/main.js
Comment on lines +706 to +707
const rec = await getEnrichment(norad);
if (!rec) { detail.innerHTML = '<p class="subtle">No catalogue record for this object.</p>'; return; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discard stale catalogue detail responses

When a user quickly selects rows from different uncached buckets, the requests can resolve out of order, and this response is rendered without confirming that its NORAD ID is still selected. A slower first request can therefore overwrite the second row's detail and make “Show in 3D” target the wrong object while the active highlight remains on the second row; add the same request/selection guard already used by showEnrichment().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 861d7f8. selectCatRow() now sets a catReqNorad request token and bails with if (catReqNorad !== norad) return; after the fetch resolves — the same guard showEnrichment() uses — so a stale response can no longer overwrite a newer row's detail or mis-target "Show in 3D".


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (32)
data/enrichment/69.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject the contradictory lifecycle record for NORAD 69729.

decayDate is set to "2026-06-26" and opsStatus is "decayed", but status remains "in-orbit". Since the decay is before the current date, this record should be treated as decayed; normalise these fields during merge/validation, fail the build when they disagree, and regenerate this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/69.json` at line 1, Update the enrichment merge/validation
logic for NORAD 69729 so a past decayDate such as 2026-06-26 normalizes
opsStatus to decayed and status to the decayed state instead of in-orbit. Add
validation that fails the build when lifecycle fields remain contradictory, then
regenerate data/enrichment/69.json with the corrected record.
data/enrichment/9.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject records with inconsistent launch dates before publishing.

Line 1 contains conflicting dates:

  • NORAD 9703: COSPAR 1973-086GB, launchDate 1977-12-21.
  • NORAD 9799: COSPAR 1976-126Z, launchDate 1971-02-25.
  • NORAD 9954: COSPAR 1976-126AU, launchDate 1971-02-25.

Fix the source join or the upstream records. Add validation that the COSPAR year matches the launchDate year before generating the shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/9.json` at line 1, Reject records whose COSPAR year differs
from the launchDate year before publishing the enrichment shard. Add validation
to the shard-generation flow that parses the year from cospar and launchDate,
excludes inconsistent records such as NORAD 9703, 9799, and 9954, and then
corrects the source join or upstream data so valid records remain included.
scripts/enrich/http.mjs-26-56 (1)

26-56: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the fetch() call.

getText() calls fetch(url, { headers, redirect: 'follow' }) with no timeout. If CelesTrak or GCAT hangs or responds slowly, this call can block indefinitely.

satcat.mjs uses this helper for the load-bearing source. A hang here stalls the entire scheduled enrichment build, since there is no time limit set on the call.

Add a bounded timeout with AbortSignal.timeout(...) so a slow upstream fails fast instead of hanging the job.

⏱️ Proposed fix to bound the fetch call
-  const res = await fetch(url, { headers, redirect: 'follow' });
+  const res = await fetch(url, { headers, redirect: 'follow', signal: AbortSignal.timeout(30_000) });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/enrich/http.mjs` around lines 26 - 56, Update the fetch call in
getText to include a bounded AbortSignal.timeout(...) option alongside the
existing headers and redirect settings. Use the project’s intended timeout
duration if already defined; otherwise introduce an appropriate finite timeout
so slow upstream requests fail promptly while preserving the current response
and caching flow.
scripts/enrich/write.mjs-51-65 (1)

51-65: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Write new bucket files before deleting stale ones.

At lines 54-56, every existing *.json file in data/enrichment/ is deleted before the new bucket files are written at lines 63-65. If the process is interrupted between these two loops, catalog-index.json (already written at line 50) will reference NORAD ids whose bucket file was deleted but not yet rewritten. Client lookups for those ids will then fail even though the index lists them.

Write the new bucket files first, and delete only the files that are not part of the new bucket set, afterwards. This removes the window where the index and the bucket files disagree.

🗂️ Proposed fix: write before delete
-  // 2. Full records, bucketed by NORAD/1000. Clear stale buckets first so a
-  //    shrunk catalogue never leaves orphans behind.
-  for (const f of await readdir(enrichDir).catch(() => [])) {
-    if (f.endsWith('.json')) await rm(join(enrichDir, f));
-  }
   const buckets = new Map();
   for (const r of all) {
     const b = bucketOf(r.norad);
     if (!buckets.has(b)) buckets.set(b, {});
     buckets.get(b)[r.norad] = r;
   }
+  const keep = new Set([...buckets.keys()].map((b) => `${b}.json`));
   for (const [b, obj] of [...buckets].sort((a, z) => a[0] - z[0])) {
     await writeFile(join(enrichDir, `${b}.json`), stringify(obj));
   }
+  // Clear stale buckets only after every new bucket has been written, so a
+  // shrunk catalogue never leaves orphans behind and a crash mid-run never
+  // leaves the index pointing at a missing file.
+  for (const f of await readdir(enrichDir).catch(() => [])) {
+    if (f.endsWith('.json') && !keep.has(f)) await rm(join(enrichDir, f));
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/enrich/write.mjs` around lines 51 - 65, Update the bucket-writing
flow after the bucket map is built to write all new bucket JSON files before
removing any existing files. Track the generated bucket filenames, then delete
only existing JSON files not present in that new set; keep catalog-index.json
and the existing bucket ordering unchanged.
scripts/enrich/sources/gcat.mjs-67-75 (1)

67-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the GCAT Status → lifecycle mapping for deep-space transition codes.

lifecycle() maps any D prefix to 'decayed', but GCAT treats the Status column as a phase-status code and uses deep-space codes such as DSO and DSA for objects that leave the Earth-bound catalog for heliocentric or Lagrange trajectories. Records such as IMAP, SWFO-L1, CARRUTHERS, and their rocket bodies now carry "status":"decayed" despite "opsStatus":"operational", which contradicts their real trajectory.

Narrow the D branch to genuine re-entry/decay phase codes, and handle deep-space transition codes separately rather than folding them into 'decayed'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/enrich/sources/gcat.mjs` around lines 67 - 75, Update lifecycle() so
D-prefixed deep-space transition statuses such as DSO and DSA are handled
separately and do not return 'decayed'. Restrict the decayed mapping to genuine
re-entry or decay phase codes, while preserving the existing orbit, landed,
empty, and unknown-status behavior.
data/enrichment/66.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Fix the cross-source join for NORAD IDs 66898 and 66903.

These records identify STARLINK-36065 and STARLINK-36077 with COSPAR IDs 2025-283W and 2025-283AB, but use launchDate: "2026-07-07", owners AXEL and ENDUR, and non-Starlink dimensions and masses. This mixes catalogue identity with fields from another payload.

Correct the source join or adapter, then regenerate the shard. Do not patch this generated file manually. Add validation for COSPAR year, launch date, owner, dimensions, and mass consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/66.json` at line 1, Fix the cross-source join or adapter
producing NORAD records 66898 and 66903 so STARLINK-36065 (2025-283W) and
STARLINK-36077 (2025-283AB) retain consistent Starlink catalogue fields rather
than unrelated launch dates, owners, dimensions, or masses. Add validation
covering COSPAR year, launch date, owner, dimensions, and mass consistency, then
regenerate the affected shard instead of editing the generated JSON directly.
data/enrichment/67.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Correct the launch-date outliers before publishing.

Records 67250 and 67251 use launchDate:"2025-11-28", but the surrounding 2025-313* records from VOSTO use 2025-12-28. These records share the same COSPAR launch designator, so the two dates are inconsistent and will expose incorrect catalogue data. Correct the source records, regenerate data/enrichment/67.json, and add a launch-group consistency check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/67.json` at line 1, Correct the source records for NORAD
entries 67250 and 67251 to use launchDate 2025-12-28, then regenerate
data/enrichment/67.json so the generated records reflect the corrected dates.
Add a consistency check that validates matching COSPAR launch groups, including
the surrounding 2025-313* VOSTO records, share the same launch date before
publishing.
data/enrichment/58.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject lifecycle-inconsistent records before publishing this shard.

Records 58015, 58049, 58050, 58307, 58752, and 58964 are marked status:"decayed" without decayDate, so their 12-month decay window cannot be checked. Record 58049 also has both status:"decayed" and opsStatus:"operational".

Update the validation stage to require decayDate on decoded records and reject contradictory lifecycle states, then regenerate this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/58.json` at line 1, Update the shard validation stage for
decoded records to require a valid decayDate whenever status is "decayed", and
reject contradictory lifecycle combinations such as status "decayed" with
opsStatus "operational". Apply this validation before publishing the shard, then
regenerate data/enrichment/58.json so records 58015, 58049, 58050, 58307, 58752,
and 58964 are corrected or excluded.
data/enrichment/10.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Exclude stale decayed records from the published shard.

Line 1 contains old records such as NORAD 10017 and 10509 with status:"decayed" but no decayDate. scripts/enrich/merge.mjs therefore does not apply the 12-month filter to them, so they remain in the catalogue despite the active/recently-decayed policy. Update the merge contract to exclude or separately classify decayed records without a valid current decay date, then regenerate this shard and its manifest.

This follows the PR objective for a 12-month decay window and the supplied scripts/enrich/merge.mjs:33-106 contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/10.json` at line 1, Update the merge contract in
scripts/enrich/merge.mjs, particularly the filtering/classification logic
between the existing decay-date handling boundaries, so records with status
"decayed" and no valid current decayDate are excluded or separately classified
rather than published. Preserve inclusion only for active records and decayed
records within the 12-month policy, then regenerate data/enrichment/10.json and
its manifest from the corrected merge output.
data/enrichment/11.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject mismatched COSPAR and launch-date years before publication.

scripts/enrich/validate.mjs only checks row counts and GCAT join fraction. As written, scripts/enrich/merge.mjs lets GCAT launchDate override SATCAT dates when joined on the same NORAD id, so records such as 11006 (1978-079C / 1979-12-07) and 11792 (1966-096C / 1979-12-24) can be published with inconsistent launch metadata. Add a validation gate that rejects records where GCAT-sourced launchDate has a different year from SATCAT-sourced cospar unless the source evidence explicitly supports the correction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/11.json` at line 1, Add a validation gate in
scripts/enrich/validate.mjs for merged records produced by
scripts/enrich/merge.mjs: compare the year from GCAT-sourced launchDate with the
year encoded in SATCAT-sourced cospar, and reject mismatches before publication.
Permit an exception only when the record’s source evidence explicitly supports
the corrected launch date, while preserving existing row-count and GCAT join
validations.
data/enrichment/12.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce the decay window before writing this shard.

Records 12255, 12732, 12927, and 12938 have status:"decayed" but no decayDate. The build cannot prove that these records fall within the stated 12-month decay window. They can remain in the catalogue and generated record counts indefinitely.

Reject decayed records without a valid decayDate, apply the cutoff to that date, then regenerate the shard and manifest. Add validation fixtures for missing and expired decay dates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/12.json` at line 1, The enrichment shard includes decayed
records without valid, window-checked decay dates. Update the shard-generation
validation to reject records with status "decayed" when decayDate is missing or
invalid, exclude records whose decayDate falls outside the 12-month cutoff, then
regenerate this shard and its manifest and add fixtures covering missing and
expired decay dates.
data/enrichment/13.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce the 12-month decay window for decayed objects.

merge.mjs currently only uses decayDate to filter decayed records. Records such as 13104, 13105, 13152, 13413, 13580, and 13901 have "status":"decayed" but no decayDate, so they are not checked against the documented 12-month retention rule and can remain in the catalogue. Update the merge/build validation to reject decayed objects without a trusted decayDate, or apply an explicit documented fallback before regenerating the shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/13.json` at line 1, Update the merge/build validation in
merge.mjs to enforce the 12-month retention rule for every object with status
decayed, including records lacking decayDate. Reject such records unless they
have a trusted decayDate within the allowed window, or implement the project’s
explicitly documented fallback before regenerating the enrichment shard.
data/enrichment/61.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Correct the cross-assigned Galileo alternate names.

NORAD 61182 is named GSAT0232 (GALILEO 32) but lists GSAT0226. NORAD 61183 is named GSAT0226 (GALILEO 31) but lists GSAT0232. This will make catalogue search and detail views identify the wrong object.

Swap the two GSAT... aliases and recheck the remaining Galileo aliases against the source before regenerating the shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/61.json` at line 1, Correct the cross-assigned GSAT aliases
for NORAD entries 61182 and 61183: ensure GSAT0232 is associated with GALILEO 32
and GSAT0226 with GALILEO 31. Verify the remaining Galileo aliases against the
source, then regenerate the enrichment shard while preserving its existing
schema and metadata.
data/enrichment/62.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject contradictory lifecycle states before publishing the shard.

data/enrichment/62.json contains incompatible lifecycle values:

  • 62460 and 62717 have status:"decayed" and opsStatus:"operational".
  • 62643 has opsStatus:"decayed" and status:"in-orbit".
  • 62485 is another status:"decayed" with opsStatus:"operational" case.

This pattern also appears across the enrichment shards, so add validation or correct precedence in the enrichment pipeline, then regenerate the outputs rather than editing this generated file manually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/62.json` at line 1, The enrichment pipeline must reject or
resolve contradictory lifecycle states before publishing generated shard
outputs. Update the validation or precedence logic handling status and opsStatus
so decayed objects cannot remain operational or in-orbit, including records like
62460, 62717, 62643, and 62485; then regenerate all affected enrichment shards
instead of editing data/enrichment/62.json manually.
data/enrichment/64.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject contradictory lifecycle states before publishing the shard.

Line 1 contains conflicting records for NORAD 64197 (TIANWEN-2) and NORAD 64527 (COSMOS 2590). Both records set status to "decayed" but opsStatus to "operational" and omit decayDate.

This can show an object as both decayed and operational in catalogue filters and detail views. Fix the merge or validation logic, then regenerate data/enrichment/64.json. Do not patch only this generated file.

#!/bin/bash
set -euo pipefail

for file in data/enrichment/*.json; do
  jq -e '
    [
      to_entries[] | .value
      | select(
          .status == "decayed"
          and (.opsStatus != "decayed" or (.decayDate | not))
        )
    ] | length == 0
  ' "$file" >/dev/null
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/64.json` at line 1, Update the merge or validation logic that
produces enrichment shard records so any object with status "decayed" also has
opsStatus "decayed" and a decayDate, rejecting contradictory records before
publication. Ensure this applies to records such as TIANWEN-2 and COSMOS 2590,
then regenerate the generated enrichment shard rather than editing it directly.
data/enrichment/18.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject cross-source launch-date and COSPAR mismatches before publishing.

Line 1 contains records where the COSPAR year differs from launchDate: 18257 (1987-006C vs 1985-09-04), 18290 (1986-019RY vs 1975-01-22), and 18608 (1985-077K vs 1987-06-09). Their _sources values show cospar from satcat and launchDate from gcat, which can corrupt date-based filters and launch-time calculations. Add validation for this invariant, fix the join/precedence logic, and regenerate the shard. Allow only documented schema exceptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/18.json` at line 1, The enrichment data includes records
whose COSPAR year conflicts with launchDate, notably NORAD records 18257, 18290,
and 18608. Add validation to reject cross-source COSPAR/launchDate mismatches
before publishing, correct the join or field-precedence logic so both values
come from a consistent source, preserve only documented schema exceptions, and
regenerate the affected shard.
data/enrichment/4.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject historical decayed objects without decayDate.

data/enrichment/4.json includes objects such as 4040, 4041, 4226, 4366, and 4368 with "status":"decayed" but no decayDate, so the build cannot place them in the 12-month window. Fail closed in the merge/validation step when decayDate is missing and regenerate the shard set, rather than publishing stale records in data/enrichment/*.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/4.json` at line 1, Reject records with status "decayed" when
decayDate is missing during the merge/validation step, failing the build rather
than emitting them into enrichment shards. Apply this validation to all
historical objects, then regenerate the complete data/enrichment shard set so
stale decayed records such as 4040, 4041, 4226, 4366, and 4368 are removed.
data/enrichment/40.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Apply the 12-month decay window to records without decayDate.

This shard contains records such as 40319, 40320, 40321, 40322, 40323, 40390, and 40391 with "status":"decayed" but no decayDate. The build cannot prove that these records fall within the configured 12-month window, so they can remain in the deployed catalogue indefinitely. Update merge validation to exclude decayed records with an unknown date, or define a bounded fallback, then regenerate this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/40.json` at line 1, Update the merge-validation logic that
filters decayed records so entries with status "decayed" and no decayDate are
excluded from the deployed catalogue, or use a bounded fallback date within the
configured 12-month window. Preserve inclusion of records with a known decayDate
only when they satisfy the existing window, then regenerate the affected
enrichment shard.
data/enrichment/44.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject contradictory lifecycle states before generating the shard.

Records 44432, 44441, and 44625 have "status":"decayed" with "opsStatus":"operational" and no "decayDate", while other decayed records in the shard have "opsStatus":"decayed" plus a decay date. This makes the catalogue show objects as both decayed and operational.

Fix the merge or validation rule that produces these records. Require "decayDate" for decayed records and resolve conflicting operational status values before regenerating data/enrichment/44.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/44.json` at line 1, Reject contradictory lifecycle states
during the enrichment merge or validation step before shard generation: records
with status “decayed” must include decayDate and use a non-operational opsStatus
consistent with other decayed records. Update the responsible merge/validation
rule, then regenerate data/enrichment/44.json so records 44432, 44441, and 44625
no longer remain operational without decay dates.
data/enrichment/22.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Correct the shifted altNames mapping before publishing this shard.

Line 1 assigns IO-26 to NORAD 22827 (HEALTHSAT 1). NORAD 22826 is ITAMSAT (IO-26), but its altNames value is missing even though _sources.altNames is gcat. CelesTrak maps IO-26 to NORAD 22826, not 22827. (celestrak.org)

Fix the source or merge assignment, add an invariant test for alternate-name ownership, and regenerate data/enrichment/22.json. Do not edit the generated shard manually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/22.json` at line 1, Correct the source or merge assignment
that currently places “IO-26” on NORAD 22827 so it belongs to NORAD 22826,
ITAMSAT, while preserving its _sources.altNames value from gcat. Add an
invariant test ensuring alternate names are assigned to the correct NORAD
object, then regenerate data/enrichment/22.json through the established
generation workflow rather than editing the generated shard directly.
data/enrichment/45.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject conflicting lifecycle states before publishing this shard.

Line 1 contains records such as 45110, 45167, 45169, 45170, 45171, 45610, 45918, and 45935 with opsStatus: "operational" but status: "decayed". These records also omit decayDate. The catalogue can therefore show the same object as operational and decayed, with no timestamp to resolve the conflict.

Define the contract between opsStatus and status, then reconcile the source values or reject the record during validation. Regenerate data/enrichment/45.json from the corrected merge output. Do not edit this generated file manually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/45.json` at line 1, Define and enforce the lifecycle-state
consistency contract in the enrichment merge/validation pipeline so records
cannot publish opsStatus "operational" with status "decayed", especially when
decayDate is absent. Reconcile conflicting source values or reject invalid
records before shard generation, then regenerate data/enrichment/45.json from
the corrected merge output rather than editing it manually.
data/enrichment/46.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve the conflicting lifecycle state for NORAD 46113.

MEV-2 is marked status:"decayed" with no decayDate, while opsStatus:"operational". This inconsistent lifecycle record can appear in the generated catalogue. Make status and opsStatus consistent, or reject this shape during generation, then regenerate the shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/46.json` at line 1, Resolve the NORAD 46113 record for MEV-2
so its lifecycle fields are consistent: do not retain status "decayed" without a
decayDate alongside opsStatus "operational". Update the source/generation data
or validation responsible for this record, then regenerate the affected shard
and verify the corrected entry.
data/enrichment/26.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Correct the generated state for NORAD 26400 before deployment.

Record 26400 identifies ISS (ZVEZDA) as opsStatus:"operational" but status:"decayed", with no decayDate. NASA updates dated April 16, 2026 and May 27, 2026 still describe Zvezda as part of the operational ISS. (nasa.gov)

Fix the source precedence or validation rule, then regenerate data/enrichment/26.json. Do not patch this generated shard manually.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/26.json` at line 1, The generated record for NORAD 26400 has
the invalid combination of operational opsStatus and decayed status without a
decayDate. Update the source precedence or validation rule governing generated
enrichment records, using the NORAD record-generation and validation symbols, so
ISS (ZVEZDA) remains operational; then regenerate data/enrichment/26.json rather
than editing the shard directly.

Source: MCP tools

data/enrichment/28.json-1-1 (2)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Resolve the conflicting lifecycle state for NORAD 28646.

This record contains "status":"decayed" and "opsStatus":"operational". _sources shows that the conflict comes from GCAT and SATCAT. Reconcile the values during merging, or fail validation before regenerating the shard. Otherwise, the catalogue can present a decayed object as operational.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/28.json` at line 1, Resolve the conflicting lifecycle fields
for NORAD 28646 before finalizing the enrichment shard: do not retain status
decayed alongside opsStatus operational. Reconcile the GCAT and SATCAT values
using the merge/validation flow, or reject the record during validation so
regeneration cannot publish contradictory lifecycle state.

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject decayed records without decayDate before writing the shard.

merge.mjs only applies the 12-month decay cutoff when decayDate is present, so NORAD objects marked status: "decayed" without that date still pass into data/enrichment/28.json; examples include 28159, 28646, 28788, and 28928. Either reject these records during validation/build or explicitly classify their decay window, then regenerate the shard and manifest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/28.json` at line 1, Reject records with status "decayed" when
decayDate is missing during the validation or build flow before shard output is
written; update the relevant merge/validation logic rather than modifying
individual records. Preserve the existing 12-month decay cutoff for records that
include decayDate, then regenerate data/enrichment/28.json and its manifest.
data/enrichment/50.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject contradictory decay metadata before publishing the shard.

Records 50463 and 50464 have status: "decayed" but no decayDate. Record 50463 also has opsStatus: "operational". These values cannot reliably support the stated 12-month decay window and can show an object as decayed without a usable decay date.

Update the merge or validation stage to require a valid decayDate for decayed records and to resolve the 50463 status conflict. Then regenerate this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/50.json` at line 1, Reject or correct contradictory decay
metadata in the enrichment merge/validation stage before publishing the shard:
any record with status “decayed” must have a valid decayDate, and record 50463
must no longer combine status “decayed” with opsStatus “operational.” Apply the
resolved metadata consistently, then regenerate the shard data.
data/enrichment/52.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject decayed records without a decayDate.

NORAD 52914 (CAPSTONE) and NORAD 52916 (LUNAR PHOTON) have "status":"decayed" but no decayDate. The 12-month decay filter cannot determine whether these records are within the retention window. NORAD 52914 also reports "opsStatus":"operational", which creates contradictory catalogue data.

Require a valid decayDate during merge or validation, or encode and handle a documented exception for non-Earth-orbit objects before publishing this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/52.json` at line 1, Reject or correct decayed records lacking
a valid decayDate during the enrichment merge or validation flow, specifically
NORAD 52914 (CAPSTONE) and 52916 (LUNAR PHOTON), before publishing this shard.
Resolve NORAD 52914’s contradictory opsStatus value as part of the same
handling, or add a documented non-Earth-orbit exception that the 12-month decay
filter explicitly supports.
data/enrichment/29.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject undated decayed records before writing enrichment shards.

scripts/enrich/merge.mjs only applies the 12-month decay cutoff when decayDate exists. This shard contains decayed records without decayDate, including 29244, 29321, 29326, 29406, 29431, 29474, 29510, 29511, and 29652. If build/validation does not explicitly reject or exclude them, they can still be written to data/enrichment/29.json and enter the published catalogue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/29.json` at line 1, Update the enrichment merge flow in
scripts/enrich/merge.mjs to reject or exclude decayed records lacking decayDate
before shard output is written. Ensure records such as 29244, 29321, 29326,
29406, 29431, 29474, 29510, 29511, and 29652 cannot enter
data/enrichment/29.json, while preserving the existing 12-month cutoff for
records with decayDate.
data/enrichment/32.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject the contradictory decay status for NORAD 32474.

This record has decayDate: "2026-07-25" but status: "in-orbit". July 25, 2026 is before the current date, August 1, 2026. The catalogue can therefore display a decayed object as in orbit.

Make the merge or validation stage force status: "decayed" when decayDate is in the past, or reject the record. Regenerate this shard instead of editing the generated JSON directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/32.json` at line 1, The generated shard contains a
contradictory NORAD 32474 record whose past decayDate is paired with status
"in-orbit". Update the upstream merge or validation stage to force status
"decayed" whenever decayDate is before the current date, or reject the record,
then regenerate this shard rather than editing the JSON directly.
data/enrichment/33.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reconcile status with decayDate before publishing enrichment shards.

data/enrichment/33.json contains records such as 33901 where decayDate is 2026-07-29 but status remains in-orbit. Records with present or past decay dates should not be reported as in orbit. Apply this rule across data/enrichment/*.json, update the lifecycle derivation/merge step, regenerate the shards, and reject inconsistent records before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/33.json` at line 1, Reconcile lifecycle fields in the
enrichment derivation/merge step so any record with a present decayDate on or
before the applicable reference date cannot retain status “in-orbit”; derive the
appropriate decayed status consistently across all enrichment shards. Add
validation that rejects inconsistent records before merge, then regenerate every
data/enrichment shard, including 33.json, with corrected values.
data/enrichment/35.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not ship status:"decayed" records without decayDate.

data/enrichment/35.json includes decay-cycle inconsistencies: 35315 is both status:"decayed" and opsStatus:"operational" with no decayDate, while 35539, 35568, 35787, 35939, and 35952 are decayed with missing decay dates. The 12-month window needs a date to validate those retained records. Make the build fail closed, exclude decayed records without a validated date, or reconcile the lifecycle source before writing this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/35.json` at line 1, The enrichment shard contains records
marked status "decayed" without a validated decayDate, including conflicting
lifecycle values. Before writing data/enrichment/35.json, fail closed or exclude
every decayed record lacking a valid decayDate, and reconcile conflicting
status/opsStatus values such as record 35315 so no invalid decayed records are
shipped.
data/enrichment/38.json-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject decayed records without a decay date.

Record 38104 has "status":"decayed" but no decayDate or decay-date provenance. As at 1 August 2026, the 12-month retention rule requires a date on or after 1 August 2025. Require a valid decayDate during validation, or exclude 38104, then regenerate this shard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/enrichment/38.json` at line 1, Remove record 38104 from the enrichment
shard, or provide a valid decayDate with corresponding _sources.decayDate
provenance meeting the 12-month retention cutoff of 1 August 2025, then
regenerate the shard while preserving the existing validation rules.
🧹 Nitpick comments (2)
index.html (1)

43-43: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

role="tab" on a dialog-opening button is a semantic mismatch.

#mode-catalogue sits in role="tablist" alongside #mode-tracker/#mode-reentry, which switch content in place. This button instead opens a modal dialog (aria-haspopup="dialog") and its aria-selected state is toggled independently in main.js. Assistive technology announcing it as a tab in a 3-tab set is misleading, since selecting it does not select a tab panel — it opens an overlay on top of everything. Use a plain button role with aria-controls pointing at the dialog instead.

♻️ Proposed fix
-      <button id="mode-catalogue" role="tab" aria-selected="false" aria-haspopup="dialog">Catalogue</button>
+      <button id="mode-catalogue" type="button" aria-haspopup="dialog" aria-controls="catalogue">Catalogue</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.html` at line 43, Update the `#mode-catalogue` button to remove
role="tab" and its tab-selection semantics, and add aria-controls referencing
the catalogue dialog element. Preserve its existing dialog-opening behavior and
ensure the referenced dialog ID matches the actual dialog markup.
src/main.js (1)

460-469: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Catalogue dialog has no focus trap or focus-return.

The dialog sets aria-modal="true" (index.html), but nothing here enforces the modal focus contract. Keyboard users can Tab out of the open dialog into the (visually hidden, but still focusable) page behind it, and focus is not returned to #mode-catalogue when closeCatalogue() runs. Only Escape is handled.

Extend the keydown handler to trap Tab/Shift+Tab within #catalogue, and restore focus to the opener button on close.

♻️ Proposed focus-trap addition
   document.addEventListener('keydown', (e) => {
-    if (e.key === 'Escape' && catalogueOpen) closeCatalogue();
+    if (!catalogueOpen) return;
+    if (e.key === 'Escape') { closeCatalogue(); return; }
+    if (e.key === 'Tab') {
+      const focusables = Array.from($('catalogue').querySelectorAll(
+        'a[href], button:not([disabled]), input, select, [tabindex]:not([tabindex="-1"])',
+      ));
+      if (!focusables.length) return;
+      const first = focusables[0];
+      const last = focusables[focusables.length - 1];
+      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
+      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.js` around lines 460 - 469, Extend the catalogue keydown handler to
keep Tab and Shift+Tab cycling among focusable elements within `#catalogue` while
catalogueOpen, preventing focus from reaching the background page. Update
closeCatalogue to return focus to `#mode-catalogue` after closing, while
preserving the existing Escape behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 12250f9f-5291-4807-ab37-73a0bacb20e8

📥 Commits

Reviewing files that changed from the base of the PR and between f4c5de0 and 256e177.

📒 Files selected for processing (94)
  • .github/workflows/enrich.yml
  • .github/workflows/static.yml
  • .gitignore
  • SOURCES.md
  • WORKLOG.md
  • data/catalog-index.json
  • data/enrichment/0.json
  • data/enrichment/1.json
  • data/enrichment/10.json
  • data/enrichment/100.json
  • data/enrichment/11.json
  • data/enrichment/12.json
  • data/enrichment/13.json
  • data/enrichment/14.json
  • data/enrichment/15.json
  • data/enrichment/16.json
  • data/enrichment/17.json
  • data/enrichment/18.json
  • data/enrichment/19.json
  • data/enrichment/2.json
  • data/enrichment/20.json
  • data/enrichment/21.json
  • data/enrichment/22.json
  • data/enrichment/23.json
  • data/enrichment/24.json
  • data/enrichment/25.json
  • data/enrichment/26.json
  • data/enrichment/27.json
  • data/enrichment/28.json
  • data/enrichment/29.json
  • data/enrichment/3.json
  • data/enrichment/30.json
  • data/enrichment/31.json
  • data/enrichment/32.json
  • data/enrichment/33.json
  • data/enrichment/34.json
  • data/enrichment/35.json
  • data/enrichment/36.json
  • data/enrichment/37.json
  • data/enrichment/38.json
  • data/enrichment/39.json
  • data/enrichment/4.json
  • data/enrichment/40.json
  • data/enrichment/41.json
  • data/enrichment/42.json
  • data/enrichment/43.json
  • data/enrichment/44.json
  • data/enrichment/45.json
  • data/enrichment/46.json
  • data/enrichment/47.json
  • data/enrichment/48.json
  • data/enrichment/49.json
  • data/enrichment/5.json
  • data/enrichment/50.json
  • data/enrichment/51.json
  • data/enrichment/52.json
  • data/enrichment/53.json
  • data/enrichment/54.json
  • data/enrichment/55.json
  • data/enrichment/56.json
  • data/enrichment/57.json
  • data/enrichment/58.json
  • data/enrichment/59.json
  • data/enrichment/6.json
  • data/enrichment/60.json
  • data/enrichment/61.json
  • data/enrichment/62.json
  • data/enrichment/63.json
  • data/enrichment/64.json
  • data/enrichment/65.json
  • data/enrichment/66.json
  • data/enrichment/67.json
  • data/enrichment/68.json
  • data/enrichment/69.json
  • data/enrichment/7.json
  • data/enrichment/8.json
  • data/enrichment/9.json
  • data/manifest.json
  • docs/data-enrichment-schema.md
  • docs/enrichment-build-job.md
  • index.html
  • scripts/enrich/build.mjs
  • scripts/enrich/constellations.mjs
  • scripts/enrich/http.mjs
  • scripts/enrich/merge.mjs
  • scripts/enrich/sources/gcat.mjs
  • scripts/enrich/sources/mmccants.mjs
  • scripts/enrich/sources/satcat.mjs
  • scripts/enrich/validate.mjs
  • scripts/enrich/vendor/qs.mag
  • scripts/enrich/write.mjs
  • src/enrichment.js
  • src/main.js
  • styles/main.css

…detail

- gcat: treat GCAT's 0-valued physical fields (mass/length/diameter/span)
  as missing via a positive-only `posNum()`. GCAT stores 0 as a
  "no measurement" sentinel, so `num()` was surfacing "0 kg" / "Ø 0 m"
  and, through the ?? fallback, blocking a real value in a later column.

- main: guard `selectCatRow()` against out-of-order detail responses with
  a `catReqNorad` request token, matching `showEnrichment()`. A slow first
  fetch could otherwise overwrite a newer row's detail and point
  "Show in 3D" at the wrong object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011V39eEU8VDuM2WpzKhNgBg
@Darkflib
Darkflib merged commit 6cf480b into main Aug 1, 2026
1 check passed
Darkflib added a commit that referenced this pull request Aug 1, 2026
PR #7's review fixed gcat.mjs to treat GCAT's 0-valued physical fields as
"no measurement" (posNum), but the committed seed data/ was not regenerated —
so the checked-in catalogue still showed "0 kg" / "Ø 0 m" for 12,635 records
until the next enrich.yml cron run. Regenerate it now.

- massKg===0: 12,635 -> 0; dimensions containing a 0: 12,544 -> 0
- record count unchanged (36,174); real values preserved (ISS 20,351 kg)
- diff is data/ enrichment buckets + manifest only (index carries no mass/dims)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Darkflib
Darkflib deleted the claude/data-enrichment-caching-db3a40 branch August 10, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants