Commit Graph
82 Commits
Author SHA1 Message Date
Senrokai 14f3fe8e66 Merge pull request #13 from avalon-vanguard/fix/catalogue-refresh-e2e
Follow the catalogue rather than a star it used to rank fourth
2026-08-27 19:50:09 +02:00
SenrokaiandClaude Opus 5 82d70ce871 Follow the catalogue rather than a star it used to rank fourth
The scheduled refresh on 24 August took the star catalogue from 68 388 rows to
447 410 and from 820 kB to 5.4 MB. No branch in the seven-PR chain had ever seen
it -- they were all cut before -- so nothing tested the combination until the
chain landed on main. Two things broke there.

The neighbour ring named Barnard's Star, and the spec clicked it by name. The
refresh is a Gaia DR3 merge that carries the same physical star twice: Proxima
Centauri at 1.296 pc and Gaia DR3 5853498713190525696 at 1.302 pc are one star,
as are Barnard's Star at 1.824 and Gaia DR3 4472832130942575872 at 1.828. The
extra row pushed Barnard's from fourth-nearest to fifth, off a ring that shows
four. The ring is doing exactly what it says; the spec was asserting the
catalogue's contents. It now reads whichever star the ring names and follows
that one, so the next refresh cannot demote it.

Four other assertions were timing out at Playwright's default five seconds --
not because anything was wrong, but because every one of these tests boots that
catalogue and the suite boots several at once on a software rasterizer. The
heavy waits already carried explicit longer timeouts; the default now matches.
Capping the worker count would have worked too, and would have cost every run
the time whether or not the machine needed it.

npm test 607/607, npx playwright test 16/16 at the default worker count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fcUfL82nvyh9VebX1Fz6w
2026-08-27 19:39:26 +02:00
Senrokai 42f304b30b Merge pull request #12 from avalon-vanguard/feat/hud-about
Say what is written about a world, when asked
2026-08-27 19:23:34 +02:00
Senrokai 4e2575674d Merge pull request #11 from avalon-vanguard/feat/hud-2d
Draw it flat: an orthographic plan view
2026-08-27 19:23:18 +02:00
Senrokai 3612d722c5 Merge pull request #10 from avalon-vanguard/feat/hud-bookmarks
Keep a place, and come back to it
2026-08-27 19:23:01 +02:00
Senrokai 6c5cc1ca19 Merge pull request #9 from avalon-vanguard/feat/hud-routes
Route between stars, through the crossings a chosen range allows
2026-08-27 19:22:44 +02:00
Senrokai 911d876f55 Merge pull request #7 from avalon-vanguard/feat/hud-neighbours
Name the neighbours, from inside the system
2026-08-27 19:22:28 +02:00
Senrokai 0c190539fe Merge pull request #6 from avalon-vanguard/feat/hud-scene
Point at what is selected: arcs, a leader to the card, rings on the systems
2026-08-27 19:22:12 +02:00
Senrokai c11087cb8f Merge pull request #8 from avalon-vanguard/chore/junie-align
Pin the reviewer, bound its run, and wake it when a pull request reopens
2026-08-27 19:19:32 +02:00
Senrokai ee02147a78 Merge pull request #5 from avalon-vanguard/feat/hud-dock
Dock the HUD: every tool and readout on one rail along the bottom
2026-08-27 19:19:19 +02:00
github-actions[bot] 46cb923849 Refresh the astronomical catalogues
Scheduled re-run of the ETL against the live archives. Gated on the unit suite and a production build in this same run, because a GITHUB_TOKEN push triggers no CI of its own.
2026-08-24 06:05:56 +00:00
Senrokai 59c2d8483e Merge branch 'feat/hud-2d' into feat/hud-about 2026-08-21 15:42:40 +02:00
SenrokaiandClaude Fable 5 e808f50faa Answer the review: the plan view clipped a system, and pulled its neighbours inward
Two more findings against this projection, both of the same shape as the last
two: something written to the camera that happens to be live, where the plan
view derives from the other one.

The system's own depth range — a near plane a five-hundredth of an
astronomical unit out, a far plane twenty thousand — was set on the active
camera. Entering a system with the plan view already on therefore wrote it to
a camera that re-derives near and far from the perspective one every frame, so
the range never applied and the system clipped. All three unit-space depth
writes go to the perspective camera now, which is the one they are reasoned in.

And the ring of neighbour names collapsed toward the middle of the frame. Its
placement unprojected a point on the ring, treated the offset from the camera
as a direction, and stepped a fixed distance along it — which is a perspective
construction. A parallel projection has no vanishing point to step towards:
every ray through the frame is the view direction, so normalising threw the
sideways part away. Measured before and after, from inside Sol: the two names
sat 319 and 335 pixels out under perspective, 104 and gone under the plan, and
323 and 335 with the unprojected point used as what it already is.

Verified: build clean, 596/596 unit, 16/16 end-to-end on the branch this merges
into, and the ring measured on both projections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 15:42:37 +02:00
SenrokaiandClaude Fable 5 7c16ad8627 Answer the review: a null byte in the source, and a language that may not be there
Two findings, and the first is the kind a person does not catch.

The article cache keyed on `${name}\0${qualifier}` — with the null byte written
into the file rather than escaped into the string. Git calls a file with one of
those binary and stops diffing it, and every editor between here and a reader
does something different with it. The separator was the right idea, because a
name can contain a space and `("Kepler-22 b", none)` and `("Kepler-22", "b")`
are different questions; it just has to be spelled `\0`.

And `navigator.language` is optional in the DOM's own typings and missing in
some embedded engines, where splitting it would have thrown on the first press
of About rather than falling back to English.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 15:41:38 +02:00
Senrokai 971aa66303 Merge branch 'feat/hud-2d' into feat/hud-about 2026-08-21 15:20:03 +02:00
SenrokaiandClaude Fable 5 8ceea02875 Wait for the boot as long as the boot takes
The specs added over this series each opened with an untimed check that the
canvas was there, which gives it Playwright's default five seconds. That is
the one wait in this suite that cannot be made faster: eight workers share a
software rasterizer, and what they are all waiting on is the same first paint.
Under a full parallel run five seconds is a coin toss, and it came up wrong on
the route-plotting spec.

Thirty, like every other wait in here that crosses a camera flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 15:20:00 +02:00
Senrokai 005ba07750 Merge branch 'feat/hud-2d' into feat/hud-about 2026-08-21 15:13:28 +02:00
SenrokaiandClaude Fable 5 b05324337c Answer the review: the plan view was flat against the wrong plane
Two findings, both right, and the second was the feature not doing what its own
commit message said.

The depth range stopped being updated under a plan view. It is worked out in
perspective terms — near from the distance, far from eight times it — and the
orthographic camera derives its own range from that one, so skipping the
calculation left the far plane wherever it had been when the projection
changed. Flying out to the whole Galaxy from a plan view clipped away most of
it. The range is written to the perspective camera whichever one is live now,
and the plan view goes on deriving from it every frame.

And the galaxy-scale plan looked down the celestial pole. "The plane the
current scale is read against" is this system's orbital plane inside a system,
and the galactic plane outside one — but the fallback was the scene's own z,
which is the Earth's rotation axis. The normal is the north galactic pole now,
and up is the direction of the galactic centre, so a plan of the Galaxy is
laid out the way the model that draws it is described. The arms are face-on.

Verified: build clean, 596/596 unit, 16/16 end-to-end, and the Milky Way
photographed flat from 28.3 kpc with nothing clipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 15:13:24 +02:00
Senrokai 687bc3b01c Merge branch 'feat/hud-bookmarks' into feat/hud-2d 2026-08-21 15:08:43 +02:00
SenrokaiandClaude Fable 5 d397aaa7e0 Let the Solar System be kept
The Sun's catalogue id is 0, and the readout's keep control was shown by
`@if (keepableStarId(); as starId)` — which reads zero as "there is no star
here". Of the six hundred and thirty-four systems the map can be inside, the
one nobody could keep was Sol.

Checked against null now, with a test that keeps star zero, because this is the
sort of thing that comes back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 15:08:40 +02:00
Senrokai 18e9d85a1b Merge branch 'feat/hud-2d' into feat/hud-about 2026-08-21 14:56:03 +02:00
SenrokaiandClaude Fable 5 b2cb307b60 Merge the review fixes, and take Junie's on the hit radius with them
Carries the shared reference-viewport module and the cached card lookup up from
the branch they were reviewed on, and answers the one comment left against this
one.

The orthographic branch of the star field's hit test multiplied the angular size
by the frustum's half-height and then divided the result by that same
half-height. The two cancel: `setProjection` had already sized the sprite as
`angular * halfHeight / tan(REFERENCE_FOV/2)`, so dividing back out by the
half-height leaves the reference field of view and nothing else. Both
projections are one formula over a different angle now — which is also one
fewer division by a number that is zero if the frustum ever degenerates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 14:55:59 +02:00
Senrokai 50c0351870 Merge branch 'feat/hud-routes' into feat/hud-bookmarks 2026-08-21 14:52:35 +02:00
Senrokai a381c02cd0 Merge branch 'feat/hud-neighbours' into feat/hud-routes 2026-08-21 14:51:48 +02:00
Senrokai f42337c841 Merge branch 'feat/hud-scene' into feat/hud-neighbours
# Conflicts:
#	src/app/features/galaxy-system/galaxy-system-scene.component.ts
2026-08-21 14:51:12 +02:00
SenrokaiandClaude Fable 5 e853fe312e Answer the review: one reference viewport, one lookup for the card
Two of the three comments were worth taking.

The star field and the rings drawn over it each carried their own copy of the
reference viewport and field of view the angular sizes are figured against.
They agree today, and nothing would have told anyone when they stopped: a ring
would just sit a little wide of its star at some window sizes. One module now
holds the three constants and says what they are for.

The leader line to the object card looked the card's panel up by selector on
every frame it was drawn. The host element is stable and the panel inside it
only changes when a different body is selected, so the lookup is derived once
per change instead of sixty times a second.

Left alone: replacing `positions.set([x, y, z], i * 3)` with an index-by-index
loop to avoid a temporary array per host. It runs once, over six hundred and
thirty-four stars, at bootstrap, and the version with the temporary reads
better than the version without.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 14:50:19 +02:00
SenrokaiandClaude Fable 5 bd37bb8b96 Say what is written about a world, when asked
Every figure this map shows is a measurement or something derived from one,
and it says which. What it could not do was tell you what a place is — a
radius and an eccentricity do not say that Titan is the only moon with a dense
atmosphere. An About control on a body's panel now fetches the Wikipedia lead,
and it is labelled as what it is: prose from another site, credited and linked
back, below the line where this map's own figures end.

Only on the press. Nothing is fetched while a body loads, and nothing is
fetched twice.

Three things the encyclopedia does that had to be handled, all found by asking
it rather than by guessing:

It redirects, generously — "Proxima Cen b" lands on "Proxima Centauri b" and
"Kepler-22 b" on "Kepler-22b" — so the catalogue's own names can be sent as
they are, with no mapping table to maintain.

It disambiguates. "Titan" is a list of everything called Titan, and so are
"Mercury" and "Io". Wikipedia says so in the response, and this app happens to
know the kind, so a disambiguation is retried as "Titan (moon)". Only in
English: every wiki words its own qualifiers, and inventing a translation of
one would be inventing an article title.

And it rate-limits, which it did to me while I was checking the above. A
refusal to answer is not an empty answer, so the two are separate outcomes:
"Wikipedia has no article on this" is about the world, "Wikipedia could not be
reached" is about this minute — and only the first is remembered, so a second
press is allowed to try again.

The extract is capped and scrollable. A lead can run a dozen lines, and this
panel is anchored to the top of a viewport that may be shorter than the prose.

Verified: build clean, 606/606 unit including twelve for the lookup chain, 16/16
end-to-end including three that stub the encyclopedia — this suite tests the
panel, not Wikipedia — design detector clean, and the real thing exercised by
hand against Earth, Titan and Proxima Cen b at three viewport sizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-21 14:40:51 +02:00
SenrokaiandClaude Fable 5 d9bd913458 Draw it flat: an orthographic plan view
A perspective camera leans everything away from the centre of the frame. In a
system that means the orbits are ellipses whose shape depends on where they
happen to sit on screen, so two planets on the same circular orbit do not look
like they are on the same circle. Plan view, in the Display panel, swaps the
projection for a parallel one and swings to look down the plane the current
scale is read against — the galactic plane out in the field, this system's own
orbital plane inside one. Circles are circles again, wherever they are.

Both halves are the feature and neither alone is it. The projection is what
makes the shape honest; the swing is what makes it worth looking at. Orbiting
still works afterwards, so a plan is where the view starts rather than a cage.

The engine now holds both cameras and keeps them in step, rather than making
one on demand: a camera that exists only while it is being looked through is a
camera whose pose is always one swap out of date. The orthographic frustum is
derived, never stored — it is the perspective camera's own frustum at the
current orbit distance, made parallel — which is why the camera flights work
through it untouched. They move the camera; the frame follows.

Three things had to be taught that a projection had changed.

Sprites. three.js turns an angular size into a world size only when it is
compiling against a perspective camera (SpriteNodeMaterial: `camera
.isPerspectiveCamera && sizeAttenuation === false`). Under a parallel one that
step is silently skipped and every star in the field collapses to a thousandth
of a parsec. The same arithmetic is now done in the node graph behind a
uniform, so one material serves both cameras without being recompiled — and
picking follows it exactly, since a star has to be clickable where it is drawn.

Depth. A parallel camera does not back away as its frame grows, so at galactic
framing the backdrop shell and half the Milky Way sit behind its own plane. Its
depth range is symmetric about it instead, which a linear depth buffer can
afford and a perspective one could not.

And distance. Half the map was keyed on how far back the camera was pulled —
the scale ladder, the crossfade, the label radius, the range readout — which
under a parallel projection says nothing at all, because the frustum sets the
extent. They all read one honest equivalent now: the distance a perspective
camera would need to frame the same thing.

Two defects found while verifying, both mine, both from this change:

The per-frame work was computed against the camera captured at bootstrap while
the renderer drew through the other one, so after a swap every label was
projected by a camera nobody was looking through.

And the zoom limits were derived from the orbit limits, which are in whichever
unit space the view is in. Reading them on the frame the scene swaps parsecs
for astronomical units pinned the zoom at the ratio between the two, and
leaving a system landed the view three kiloparsecs out. Zoom is a plain
multiplier on a frame the distance already sets, so it is bounded by a factor.

Verified: build clean, 595/595 unit including a new spec for the projection
arithmetic, 13/13 end-to-end including two that flatten a system and check the
ladder still knows how far out it is, design detector clean, screenshots of
both scales in both projections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 20:11:31 +02:00
SenrokaiandClaude Fable 5 307fd41be8 Keep a place, and come back to it
Restores the commit reverted off the routing branch, which is where it was
committed by mistake. The change is unmodified; only its branch is.

The map had no memory. Every visit started at the same overview, and a system
worth returning to had to be found again by name each time. A mark on the
readout and on a body's panel now keeps it, a Bookmarks tab lists what has been
kept, and choosing one goes there — a star by flying into its system, a body by
opening its page.

Local storage, not an account. This map asks nobody to sign in, and a list of
stars somebody liked is not worth a server. Every read of that store is
defensive, because it is a string a person can edit, another tab can write, and
a browser can refuse to hand over at all: a bad entry is skipped rather than
losing the rest, duplicates are collapsed since two entries for one place would
each toggle the other's control, the list is bounded so a hand-edited store
cannot decide how much this renders, and where storage is denied outright the
bookmarks still work for the visit — they just do not outlive it.

The name is stored alongside the id rather than looked up, so the list reads
before the catalogues have loaded, and a bookmark to something a later
catalogue no longer holds still says what it was instead of decaying into a
bare number.

The tab is offered even when it is empty, and says what the mark does: a tab
that appears only once you have already found the feature is a tab that never
taught anyone anything.

Choosing a kept place hands the panel back to the readout, the same move as
choosing a search result and for the same reason. That behaviour is what the
end-to-end spec caught missing — the readout it asserted on did not exist,
because the panel just used was still covering it.

Verified: build clean, 587/587 unit, 11/11 end-to-end, design detector clean,
screenshots at 1440x900 and 390x844.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 19:12:58 +02:00
Senrokai 9cdd8f9388 Revert "Keep a place, and come back to it"
This reverts commit bd3a5a4. The bookmarks work was committed onto this branch
by mistake — it belongs to its own pull request, and it has one, branched from
this branch's own tip. Reverting rather than rewinding because the branch is
published and a pull request is open against it: the diff this pull request
shows is what matters, and after this it shows the routing change alone.
2026-08-20 19:10:24 +02:00
SenrokaiandClaude Fable 5 bd3a5a48c9 Keep a place, and come back to it
The map had no memory. Every visit started at the same overview, and a system
worth returning to had to be found again by name each time. A mark on the
readout and on a body's panel now keeps it, a Bookmarks tab lists what has
been kept, and choosing one goes there — a star by flying into its system, a
body by opening its page.

Local storage, not an account. This map asks nobody to sign in, and a list of
stars somebody liked is not worth a server. Every read of that store is
defensive, because it is a string a person can edit, another tab can write,
and a browser can refuse to hand over at all: a bad entry is skipped rather
than losing the rest, duplicates are collapsed since two entries for one place
would each toggle the other's control, the list is bounded so a hand-edited
store cannot decide how much this renders, and where storage is denied
outright the bookmarks still work for the visit — they just do not outlive it.

The name is stored alongside the id rather than looked up. That way the list
reads before the catalogues have loaded, and a bookmark to something a later
catalogue no longer holds still says what it was instead of decaying into a
bare number.

The tab is offered even when it is empty, and says what the mark does. A tab
that appears only once you have already found the feature is a tab that never
taught anyone anything.

Choosing a kept place hands the panel back to the readout, which is the same
move as choosing a search result and for the same reason: the panel has done
its job and the thing to look at is now the scene. That behaviour is what the
end-to-end spec caught missing — the readout it asserted on did not exist,
because the panel that had just been used was still covering it.

Verified: build clean, 587/587 unit, 11/11 end-to-end including a spec that
keeps Earth, leaves the page, comes back to it from the list and forgets it,
and one that keeps Proxima Centauri, flies out to the field and flies back in
by what was kept. Design detector clean, screenshots at 1440x900 and 390x844.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 19:07:46 +02:00
SenrokaiandClaude Fable 5 68a919bd84 Route between stars, through the crossings a chosen range allows
The map could say where a star is and what is near it, and nothing about
getting from one to another. This adds the question and the answer: pick a
departure and a destination, choose how far a single crossing may be, and get
the chain — how many jumps, how far in total, and every star on the way, each
one a step you can fly to.

A jump link is not a feature of space. There are no corridors out there; a
link is a question asked of the catalogue, which is why the range is the
user's control rather than a constant. Two facts about that catalogue decide
what the answers look like, and both are stated in the code because they read
as defects otherwise. It is magnitude-limited, so it is dense around the Sun
and thins with distance — within 50 pc a 3 pc range links 99% of it into one
piece, while over the whole 250 pc reach the same range leaves most stars
alone. And a gap in it is a gap in what has been catalogued, not in what is
there.

That is why "no route" is not the end of the answer. Where no chain exists at
the range asked for, the panel says which range would open one — the chain
whose longest hop is as short as possible, found by the same search with the
cost of arriving somewhere being the worst hop taken rather than the sum — and
offers that number as a control to accept.

Departure defaults to wherever the view already is, so one field is usually
enough. Sol to Vega at 3 pc: four jumps, 10 pc, by way of Barnard's Star,
Struve 2398 B and HD 155876. Narrow it to 0.8 pc and it says 2.26 would reach.

The graph is drawn as one buffer of line segments and the route as a second,
brighter one over it, with the graph stepping back while a route is up: near
the Sun the links are a haze, and a thread through a bright cloud is not a
thread. Both fade out with the local layer, since from outside the Galaxy the
graph is a smear.

Two measurements shaped this. Asking the index for each star's neighbours in
turn — sixty-eight thousand sorted lists, thrown away — took eight seconds; the
grid now walks its own cells once and pairs them, which takes a quarter of one.
And the range control emits per pixel dragged, so the rebuild waits for the
hand to settle.

Three defects fixed on the way, all older than the routing:

hud-acquire animated with fill-mode `both`, which leaves its closing keyframe
applied for good — and that keyframe carries a clip-path. Every panel wearing
it has been clipping its own box ever since, so anything that had to escape
one was cut away and could not even be clicked. Nothing had needed to escape
until this panel's dropdown opened upward.

The routing fields returned nothing when typed into before the catalogue
finished loading, and stayed nothing until the next keystroke. The options are
derived from the query and the index together now, so they appear when the
second of the two arrives, whichever that is.

And a link was `3-7` walking one way and `7-3` walking the other, which is two
links to anything comparing them.

Verified: build clean, 571/571 unit, 9/9 end-to-end including two new specs —
one plotting Sol to Sirius, one narrowing the range until there is no route and
accepting the one it names — design detector clean, screenshots at 1440x900
and 390x844.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 18:39:22 +02:00
SenrokaiandClaude Fable 5 c1e7f6363d Pin the reviewer, bound its run, and wake it when a pull request reopens
Junie has been reviewing pull requests here since the key was added, and eight
of the last eight runs succeeded. What it was not, was configured like the
sibling repositories, and three of those differences are worth closing.

The action was referenced as `@v1`. It is the only third-party action in this
repository and the only one handed a repository secret, so its definition
should not be able to change under us. `v1` and `v1.7.5` resolve to the same
commit today — the pin is not about which code runs now, it is about who gets
to decide that later.

There was no timeout. A run that goes wrong hangs rather than stops, and the
pull request shows a pending check until Actions gives up on its own six hours
later. Forty-five minutes, not the thirty the siblings use: the longest review
this repository has actually had ran thirty-five, on the largest diff so far,
and a ceiling that cuts a successful review short is worse than none.

And a pull request closed and reopened had had no review since it was closed.

Left alone deliberately: the absent-key step, which says so in the run summary
instead of failing a pull request for a reason that has nothing to do with its
code, and the lack of a `branches:` filter — work here stacks feature onto
feature, and filtering on main would skip every pull request in a chain but the
last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 17:59:15 +02:00
SenrokaiandClaude Fable 5 a6b30e84f0 Answer the review: a gradient over the names, a zero-distance neighbour, two lost docblocks
Three findings out of forty-one raised survived being argued against, and all
three were real.

The vignette was painting over the label layer. It lived in the HUD component,
which sits after the label host in the same stack with neither carrying a
z-index, so paint order was tree order and a decorative gradient was laid over
the names — worst at the edge of the frame, which is precisely where the
neighbour ring is. Composited, the ghost's distance line fell to 4.02:1, under
the floor the CSS next to it claims. The gradient is scene chrome rather than
HUD chrome, so it moves down between the canvas and the labels; the authored
contrast then holds as written, and every label near the edge — planets
included — is read against the sky rather than through a wash of void.

A binary companion printed "0.00 pc". A catalogue holds a close pair as two
rows at one position, so the nearest neighbour to one of them is the other,
zero away — the exact string the readout deliberately suppresses for a star's
distance from itself. The query now asks wide and drops any separation that
prints as no separation, compared through the formatter rather than against a
hand-picked epsilon so the rule survives the formatter changing.

And the edit that added all this had been spliced between updateSystemLabels'
docblock and its body, leaving a paragraph about labelling planets sitting
over the neighbour resolution and the method it described with nothing, plus
two divergent copies of the same fourteen lines seventy-five apart. Both are
back where they belong.

Verified: build clean, 558/558 unit, 7/7 end-to-end, design detector clean,
screenshots re-read in Sol and Proxima Centauri.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 17:40:10 +02:00
SenrokaiandClaude Fable 5 44c6a1f15e Name the neighbours, from inside the system
A system view could say everything about the star it was inside and nothing
about where that star was. The four nearest catalogue stars are now named
around the edge of it, each with its distance, each a button that flies there
— so a chain of neighbours can be walked without pulling back out to the field
between hops.

These are bearings, not sky positions, and that is the one deliberate
compromise here. A true direction was tried first and does not work: at this
field of view the visible cone is about 30 degrees, so on average one
neighbour in fifteen falls inside the frame — measured, not guessed, at one
label of four in Sol and none at all after a small orbit. What survives the
ring is the half of the direction a viewer can act on, which way to turn to
face it, and the ring reads as instrument rather than as scene because it sits
at a fixed radius. Real distance was never an option: Proxima is 268 000 AU
from Sol, thirteen far planes out, so the distance goes on the type line.

Proximity is answered by a new pure module rather than by a scan. A uniform
grid over the catalogue answers both "the k nearest to this star" and "every
star within n parsecs", the second being what the jump-link graph in the next
PR is built from — one scan per node, and the quadratic would show. Its spec
pins the grid against a brute-force sweep of a pseudo-random cloud, because a
spatial index is an optimisation and never a different answer.

Where the ring meets the HUD, the HUD wins: placement is given the boxes the
readout, the strip and the object card occupy, and slides a name along the
ring until it clears them, or drops it rather than print it half hidden. That
rule is a pure function with its own spec.

Four defects found while verifying this, three of them older than it:

The dock's flex column was pointer-events-auto and as wide as its strip, so
an invisible band above the strip swallowed every click in it — including,
but not only, a neighbour's. The column is transparent now and each surface
opts back in.

The ring was sized against the frame's height alone, which on a phone held
upright put it a viewport and a half wide: no neighbour was reachable on any
portrait screen. It is sized against the shorter side.

Picking a search result reopened the readout, which on a narrow viewport is a
sheet over most of the scene — reopening it onto whatever was just flown to.
Below sm it now folds away.

A selectable label's two lines are adjacent spans, so it announced as
"Sirius2.64 pc"; it carries an explicit label saying what it does.

Verified: build clean, 558/558 unit, 7/7 end-to-end including a new spec that
flies Sol to Barnard's Star by its label, design detector clean, screenshots
at 1440x900 and 390x844 in Sol and Proxima Centauri, and the keyboard path
walked: both names are in the tab order, focusable, with the accent ring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-20 17:06:25 +02:00
SenrokaiandClaude Fable 5 7591bcc0ea Point at what is selected: arcs, a leader to the card, rings on the systems
Four things the scene did not yet say, all about where to look.

Selection. Hovering or pinning a body raised its card in the corner, but
nothing in the scene said which point the card was about. Two hairline arcs
now bracket the body — the one mark borrowed from the ARK's control disc — and
a leader runs from their rim to the card's near edge, in screen space, once
per frame, because the body moves and the card's height depends on its
content. The selected body's own label swaps to the left of its point, since
the leader leaves the right and would otherwise cross the text.

Labels choose a side. Right by default; left when the text would run off the
right of the view, or into the reach of a label already placed to the right,
and never left when that would run off the left. The overlay hangs the label's
near edge on the point either way, so the hairline still meets the star.

Rings on the systems. A faint accent ring on every star known to host
planets — the one binary fact about a point of light worth reading at a
glance from the neighbourhood, since it is the one thing that says "there is
somewhere to go here". Drawn the way the star field draws stars, as
unattenuated instanced sprites with the ring a band of the quad's own uv, so
they sit on the field's points at any zoom; a first cut as three.js Points
rendered nothing at all under the WebGPU renderer. 634 of them are a lot at
the overview, so they are faint, small, fade with the local layer, and have
their own toggle — Systems — in the dock's Display tab.

Verified: build clean, 537/537 unit, 6/6 end-to-end, design detector clean,
screenshots at the overview, at 90 pc, and in Sol and Proxima with a body
hovered and pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-19 02:38:06 +02:00
SenrokaiandClaude Fable 5 8b507427d8 Answer the review: sky toggles in system view too, one media query, idempotent openSearch
Three findings from the automated review, all confirmed before acting.

The Sky toggle did nothing in system view. Its write lived only in the galaxy
crossfade, which the tick parks while the system group is up — and the sky is
still on screen there. It is now also written on toggle, in applyDisplay. The
suggested form of that fix crashed the scene on mount: the effect that calls
applyDisplay fires once at construction, before the engine has a scene, and
getScene() throws — the dock rendered no tabs at all. Guarded on
engine.isInitialized; verified with a screenshot of Proxima with the sky off.

isWideViewport built a fresh MediaQueryList on every document pointer-down;
one module-level query is read instead. And openSearch in the e2e support
clicked the Search tab unconditionally, which would fold it closed if a test
ever called it while already open; it now checks aria-selected first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-18 21:35:52 +02:00
SenrokaiandClaude Fable 5 4a1cc5240a Dock the HUD: every tool and readout on one rail along the bottom
The overlay had grown by accretion: a search box floating top-centre, a
readout panel bottom-left, a range readout bottom-right, and nothing that
said these were parts of one instrument. This puts them on one rail across
the bottom of the viewport — the dock — with a tab strip pinned to the bottom
edge and whichever panel is open growing upward from it. The top of the
screen keeps only the scale ladder and the nameplate, so the map itself is
what fills the frame.

Three tabs. SEARCH is the old search, with its field pinned to the bottom of
the panel and the results growing upward above it, so the thing being typed
into never moves while the list grows. READOUT is the old bottom-left panel.
DISPLAY is new: five layer toggles — labels, orbits, grid, deep sky, sky —
each a real scene object switched by visibility, except the ones the galaxy
crossfade already rewrites every frame, whose toggles fold into that
crossfade instead of fighting it. The range readout sits on the strip itself,
so it is readable whatever is open.

Behaviour worth stating: choosing a search result hands the panel straight
back to the readout, since the thing to look at is now the scene. `/` opens
the search from anywhere. Below `sm` the dock is the strip alone; a tap opens
a panel as a sheet, a tap on the scene folds it away. The body-detail page
gets the same dock with only the search — the info panel is its reading.

Two things found on the way. CSS2DRenderer gives every label its own
z-index for depth order, and the label host created no stacking context, so
labels painted over every HUD panel; `isolate` on the host keeps them under.
And starmap-hud's readout tests were really tests of the panel that moved,
so they moved with it.

Verified: build clean, 535/535 unit, 6/6 end-to-end, design detector clean,
screenshots at 1440×900 and 390×844 across galaxy, galactic, system,
body-detail, all three tabs and the layers-off state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-18 20:57:10 +02:00
Senrokai 9ae87e83fb Restyle the HUD as an observatory instrument (#4)
Restyle the HUD as an observatory instrument
2026-08-17 18:17:18 +02:00
Claude 650b3b37a0 Fix what the review confirmed: anchoring, overlap, honesty, one voice
Fourteen findings from an adversarial review pass, applied together because
most share two roots — geometry written against assumptions the renderer does
not hold, and a design language spelled out longhand until it drifted.

Correctness. The leader line pointed at empty space: CSS2DObject centres a
label on its anchor by default, and margin only nudges the centred box — the
overlay now anchors the left edge (center 0, 0.5) so the hairline meets the
star. The info panel and not-found panel returned to top-4 at sm, under a
search field that is 26rem wide and centred, covering the back button on
every viewport from 640 to 1088px; they now wait for xl. The search empty
state asserted "nothing matches" while the catalogues were still loading —
and forever if they failed — so it now waits for the index. "Matches 8" was
the page size wearing the costume of a count; the header now reports the
real total, with the cap stated when it bites. The nameplate un-hides at lg
instead of sm, clear of the rail and the object card. theme-color matches
the void again.

Accessibility. Tabbing to search changed one hairline's hue; the wrapper now
carries the old ring as a visible focus indicator.

Structure. hud-surface names the panel recipe that had been inlined at seven
sites and had already forked into /85 and /92; the backdrop blur it carried
is gone from every near-opaque panel — the canvas beneath redraws every
frame, so each blur was re-sampled continuously for an effect the fill hid —
and survives only as blur-sm on the genuinely translucent search shell.
type-label and type-eyebrow name the two label voices, retiring 0.14em and
the stray hover:bg-accent/10. The Measured/Derived rows both cards pasted
twice each live once in ReadoutSectionsComponent; the reticle and chevron
each draw from a single geometry, the reticle's stroke held in screen pixels
so one mark serves every size. The acquire wipe plays once per search, not
once per keystroke, because both outcomes now share one panel. hud-banner,
which styled nothing since the rule was deleted, is a data-testid — the
convention its own file already used.

527 unit tests, 6/6 end-to-end, production build, and a driven screenshot:
every label now hangs off its star with the hairline touching the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-17 15:19:53 +00:00
SenrokaiandClaude Fable 5 0dcd49c777 Merge main again: readouts built once, said in the instrument's voice
Main moved while the previous merge was being verified. It brought the
shared bodyReadouts builder — one source for a body's measured and derived
rows, used by the detail page and the system view's new object card — plus
the derived-value asterisk in the HUD readout panel.

All of that data flow is kept. The templates it arrived in are restyled to
this branch's idiom: the info panel and the object card share the same
organism (header, full-bleed readout rows, provenance line, route rail),
the object card's route rail sits at the bottom because there the route is
the next step rather than the way back, and the derived asterisk and its
footnote keep their meaning in sentence case. The card also needed the
restyle to render at all — it arrived wearing hud-panel, a class the
observatory system no longer defines.

Verified: build clean, 527/527 unit, 6/6 e2e, design detector clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-17 16:16:50 +02:00
SenrokaiandClaude Fable 5 a965aed1db Merge main, carrying the observatory instrument onto its rebuilt chrome
Two HUD redesigns happened in parallel: this branch restyled the old chrome
as a precision observatory instrument, while main rebuilt the chrome against
screenshots of the Star Citizen starmap — a scale ladder, a nameplate, a
readout panel, and two-line labels that say what a thing is, not just what
it is called.

This merge keeps everything main's rebuild learned and says it in this
branch's voice. Kept: the ladder's reachability semantics and test hooks,
the two-line name/kind labels and their shared declutter logic, system-view
body labels, every readout. Restyled: chamfered clip-path panels become
hairline frames with corner-tick brackets; Orbitron is gone and one readout
face carries the hierarchy; the hexagon reticle becomes the same
circle-and-ticks mark the search field wears; focus states move to real
focus-visible outlines; the long uppercase caveat notes drop to sentence
case so they read as sentences.

Two merge-borne fixes along the way: the labels' translate offset moved into
.map-label as a margin (CSS2DRenderer rewrites the inline transform every
frame, the margin is the offset it cannot touch), and the info panel's new
Derived section picked up the horizontal padding it lost when the panel
moved to per-block padding.

Verified: build clean, 496/496 unit, 6/6 e2e (on a free port — 4300 is
occupied on this machine), design detector clean, screenshots reviewed at
1440x900 and 390x844 across all four views plus search states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-17 16:09:33 +02:00
Claude 8e55964b14 Refresh the catalogues on a schedule, and republish when they change
Mondays 05:23 UTC: re-run the ETL cold against the live archives, and if the
output differs by a byte, gate it on the unit suite and a production build,
commit it to main, and dispatch the Pages deploy. If nothing changed, say so
in the run summary and touch nothing.

The gates run inside this workflow because they cannot run after it: a push
made with GITHUB_TOKEN fires no push workflows at all — GitHub's recursion
guard — so an unguarded push would deploy nothing and be checked by nothing.
The same guard is why the deploy and a visible CI record are dispatched
explicitly afterwards; dispatch events do go through where push events do
not. ci.yml gains a workflow_dispatch trigger for exactly that call.

Also corrects pages.yml's claim that configure-pages enables Pages on first
run. It cannot: the action's `enablement` input requires an admin-scoped
token, which GITHUB_TOKEN is not. If the site has never been enabled, the
first deploy fails at that step and the one-time fix is Settings → Pages →
Source: GitHub Actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-17 14:05:42 +00:00
Claude 9a5e41495b Make a cold-cache ETL run a pure function of the archives
Two order dependencies surfaced while designing the scheduled refresh, both
invisible on a developer machine with a warm cache and both guaranteed to
haunt a weekly CI run that starts cold.

The Exoplanet Archive query had no ORDER BY, so the archive was free to
return rows in any order it liked — and exoplanets.json preserves row order,
so a re-run could rewrite the file, and commit a diff, when nothing was
actually published. Ordered by pl_name, which is unique among default_flag=1
rows, so the order is total.

The deep-sky sort tie-broke with localeCompare, whose collation belongs to
the ICU build of whichever Node runs the ETL. Compared code points instead:
the file must not reorder because the runner's ICU disagrees with the machine
that wrote it last.

Both may reorder the committed files once, on the next real refresh. After
that, byte-identity between runs means what it should: the archives
published nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-17 14:05:41 +00:00
SenrokaiandClaude Opus 5 836816ffbd Restyle the HUD as an observatory instrument
Replaces the generic rounded dark cards floating over the scene with a
single instrument language: hairline frames cut by accent corner ticks, a
viewport frame that reads the whole screen as one panel, and readouts whose
figures are what the eye lands on.

- styles.css: retune the tokens (deeper void/panel, brighter text/muted so
  every label clears 4.5:1 even over the brightest part of the skybox) and
  add two utilities — `hud-brackets` draws the four corner ticks from eight
  background gradients so no panel pays for them in DOM, `hud-acquire` wipes
  a panel down on mount as its one authored moment (reduced-motion aware).
- Drop Orbitron: every string in this UI is a measurement, an identifier, or
  a catalog label, so one readout face carries the hierarchy through weight,
  size, and tracking — and the HUD paints one font request sooner.
- Search: reticle mark instead of a magnifier, transparent field inside a
  framed shell, results as a dense two-column list with a match count, and
  the previously missing empty state.
- Info panel: return rail, name/class header, and a readout table with
  tabular figures and the unit tinted back off the number.
- Star labels: a hairline leader back to the star they name. The offset moved
  from `translate-*` to a margin because CSS2DRenderer overwrites the inline
  transform every frame, which silently beat the old classes.
- Both top-anchored overlays drop below the search field under `sm` so they
  no longer stack on top of it on a phone, and focus moved from a removed
  outline plus ring to real `focus-visible` outlines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
2026-08-07 17:42:14 +02:00
Claude 7d07921d69 Say what the project is, in the two places people arrive
The README opened without mentioning that the thing is deployed, and the HTML
document carried a title and nothing else — no description, no link preview.
Both matter more now that there is a public URL to land on.

README: the live link up top; the object card and the measured/derived split
from #3, which were built but never written down; `shared/format/`, `public/`
and `.github/workflows/` in the layout.

index.html: a description, a theme colour matching --color-void so the browser
chrome does not flash white around a black sky, and Open Graph/Twitter tags so
a shared link renders as something other than a bare URL. Those URLs are
absolute and name the deployment outright — a scraper has no document to
resolve a relative path against, so they cannot follow <base> the way the rest
of the app's paths do. A custom domain later means editing these three lines.

The preview image is the galactic view, copied into `public/` so it is served
by the site itself rather than linked out to raw.githubusercontent.

Verified: the built document keeps every tag, the base href still rewrites to
/star-map/, the image answers 200 at the path the og:image names, and both the
root and a deep link still render. 527 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-07 14:22:29 +00:00
Claude 433bcca8a2 Stop publishing the repo as a plugin marketplace, and refresh the run skill
Three small things.

Removed `.claude-plugin/marketplace.json`. It is the file `/plugin
marketplace add` reads, so without it this repo no longer offers itself as a
marketplace — which is the odd part of an Angular app carrying one. The
caveman plugin's own files stay, so it can be listed from a marketplace of its
own later; only the listing is gone.

That left four documents asserting an install path that no longer exists,
including a README section handing out `/plugin marketplace add` and
`/plugin install` commands that would now fail. All four now say what is
actually true. `.junie/plans/nasa-star-map.md` still describes the repo as
containing only a marketplace, and is left alone: it records what was here
before the app was written, and is not a claim about the present.

The run skill's numbers had drifted a release behind — 496 tests in 28 files
against a real 527 in 31, and a build timed at 9s that now takes 12. Measured
rather than guessed. The pinned Playwright version it names was correct.

It also gains the Pages build, which is not a plain `npm run build`: the base
href and the 404.html copy are both required for a project site, and the
artifact root is `dist/star-map/browser` rather than its parent. Plus the
matching gotcha, since a Pages build served at `/` looks broken in a way that
tells you nothing about whether it would work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-07 14:02:12 +00:00
Claude cca652d71f Publish the map to GitHub Pages
Builds on a push to main and deploys the result, so the thing is reachable
without checking it out and running a dev server.

Two details a project site needs that a root deployment does not:

The base href is set from the repository name at build time. The app is served
from a subdirectory there, and `DataLoaderService` fetches its catalogues with
relative URLs — those resolve against `<base>` rather than the current path, so
without it a deep link would ask for /body/assets/data/stars.bin.

Pages serves a static tree with no rewrite rules, so /body/mars has no file
behind it and returns 404. Answering that 404 with the app lets the router
render the route. The status stays 404, which crawlers will notice and readers
will not; the alternative is hash URLs, which everyone notices.

Verified against a server that mimics both behaviours: the root loads the star
field with all six catalogue assets at 200, and /body/mars boots through
404.html and renders Mars at 3,390 km with its assets still resolving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-07 13:22:01 +00:00
Claude cfad718b0a Check develop after a merge, not just main
`develop` became the integration branch when #3 merged into it, but CI's push
trigger still named only `main` — so the merge commit itself ran nothing. A
pull request is checked before the merge, not after, which leaves the state of
the branch people actually build from unverified whenever two green pull
requests conflict semantically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-07 12:46:14 +00:00
Senrokai ca89724b88 Say what is known about a world, and how it is known (#3)
Fills the gaps in the HUD and the body cards, distinguishes measured
quantities from derived ones, raises an object card over the live system view
instead of navigating away from it, and adds a Junie code-review workflow
beside CI.

Five commits kept separate: the feature, the review follow-up collapsing
bodyReadouts, a doc-comment fix, the review workflow, and the fix that stops
that workflow failing a pull request when no API key is set.
2026-08-07 14:26:37 +02:00