Skip to content

Universe Glam: the pink brand system, and the gates that keep its roles apart - #7

Merged
bitcoinuniverseadmin merged 44 commits into
developfrom
feat/universe-glam-pink
Aug 28, 2026
Merged

bitcoinuniverseadmin merged 44 commits into
developfrom
feat/universe-glam-pink

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

What this is

Universe Explorer shipped an ultramarine identity that belonged to no other
Bitcoin Universe surface. Core, Wallet and Inscribe are all built on the same
pink-forward system, so the explorer read as a different company's product
wearing our name. This replaces the brand layer.

Why pink can be loud here

One rule makes it work at this density: pink is identity and intent, never a
fact about Bitcoin.
It marks the mark, the primary action, active navigation,
selection and live surfaces. It is never a status, a protocol, a fee band or a
quantity, and CI fails the build if a brand role drifts within 25 dE of a state
or protocol colour.

So the fee scale is untouched, green still means proven, red still means
unavailable, and Bitcoin orange stays where it means Bitcoin.

Values were derived, not picked

#ff0066 is the Bitcoin Universe anchor and it is 3.85:1 on white, which is
exactly why it paints borders, rings, rules and display type and never a small
label. Filled controls take #c40059, measured at 6.01:1 in both directions.
The chart series were solved for separation under protanopia, deuteranopia and
tritanopia as well as normal vision.

The research behind it

A visual language pass read each competitor's declared shell colour out of its
own markup and found the category uniform: dark and cool, with a documented
convention of low chroma. Light, warm and chromatic is an empty position, and
the pass checked whether it is empty for a good reason before taking it. Two
further passes on different strategies produced no new competitor, capability
class or trend, which is the closure condition.

docs/research/visual-language-benchmark.md holds the method, the
measurements and the sources.

What the gate found

Adding the role-separation check surfaced three collisions that predate this
change:

  • ordinals orange sat 11.1 dE from the amber that means partial evidence
  • stamps and op data sat inside the new brand family
  • the largest mining pool was drawn in #D81B60, 6.7 dE from the brand pink

Defects fixed along the way

A loud colour is useful that way. It shows you where the hierarchy was already
broken.

  • The range selector painted all eleven periods as filled primary buttons and
    marked the choice with .active alone, so nothing answered "which period am
    I looking at". Same for the Mempool/Mining switcher and the RBF filter.
  • The label above a number on a stat card took the brand colour, on 45 of them.
  • The chart zoom slider was drawn from the charting library's defaults, a pale
    blue measuring about 1.15:1 against the page, on eighteen charts.
  • The error detail under a failed panel read (undefined undefined: ) for any
    failure that was not an HTTP response: the pipe read .status off whatever it
    was handed, and its null guard came after the property access.
  • The coverage doc check compared bytes, so a CRLF working copy always looked
    stale and it could only ever pass on the Linux runner.

Checks

  • 343 palette pairings, 0 failing, including brand gradient labels measured
    through the gloss layer in all three themes
  • 154 frontend unit tests
  • production build green, no retired ultramarine value in any built stylesheet
  • branding, origin, asset, text and colour gates pass on source and on dist
  • pink paints on :root, so there is no first-frame flash

Note on sequencing

This branch is based on origin/develop at 09ec717 and was developed in a
separate worktree so it could not collide with the work in #6. It should merge
after #6 lands.

🤖 Generated with Claude Code

bitcoinuniverseadmin and others added 30 commits August 28, 2026 09:51
Enabling the database made the backend exit at startup. updatePoolsJson
fetches the pools-v2.json sha from GitHub, that fetch must not happen at
runtime here, so currentSha stayed null and index.ts aborted before it
could serve anything.

Bundle the pool list in the repository and identify it by the git blob
hash of its own bytes. That is deterministic, needs no network, and is
directly comparable with the sha already stored in the state table, so a
redeploy with unchanged pool data does no work. The bundled file is the
default now; the GitHub URLs stay for anyone who wants them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rent one

The frontend decided on its own that Mining and Charts were available.
The backend decided separately, from DATABASE.ENABLED and
INDEXING_BLOCKS_AMOUNT, whether to mount the routes behind them. Nothing
compared the two answers, so production shipped both pages against routes
that were never registered and every request behind them answered 404.

Give the two one place to agree on. The route setup records what it
actually mounted, /api/v1/capabilities reports that alongside dependency
reachability, coverage and lag, and startup refuses a configuration that
would advertise a feature it cannot serve. The rules live in their own
module with no imports, so the release gate and the tests judge exactly
what the running backend judges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every $listXX range caught its database error, logged it, and returned an
empty array. The route then answered 200 with [], so a database that was
down was indistinguishable from a chain with nothing to show, and the
Charts page rendered an empty state over a real outage.

Throw instead. statistics.routes already turns a thrown error into 500,
which is the answer the frontend needs in order to tell unavailable apart
from empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Charts page subscribed with one next handler and no error handler. A
404 killed the subscription, so isLoading stayed true, the spinner stayed
on screen, and the range buttons stopped working for the rest of the
session. Every mining widget had the same shape through an async pipe with
an else skeleton, so a failed read left 35 skeletons that nothing would
ever clear.

Add one helper that turns a request into a state machine which always
reaches a terminal state: data, empty, stale, or error. It keeps empty
apart from failed, bounds the wait, retries only what a retry could fix
with backoff and jitter, cancels an obsolete request when the range
changes, and keeps the last good answer to show as stale rather than
replacing real numbers with a blank panel.

Charts, reward stats, pool ranking and the hashrate chart use it. Each
owns its own state, so one failing module no longer decides what the rest
of the page shows. The shared status panel names what failed and offers
retry and system status, and its spinner is drawn from theme tokens: the
one it replaces was white on a light background, which is why a failed
page and a blank page looked the same.

Pool ranking and the hashrate chart also started one request from route
setup and another from the chain tip. They share one trigger now, debounced
so a burst of tips around a new block is one refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last release passed every check and still shipped a Charts page and a
Mining dashboard whose backend routes were never mounted. CI answered from
fixtures, so nothing it ran could have noticed: the thing that was wrong
was the configuration of the machine, and no check looked at that.

release.sh installs beside the running release and refuses the symlink
swap unless the build is complete, the configuration is coherent, the
database answers, the source registry parses with every token present, and
every protocol the registry calls readable has an authority configured.
After the swap it reads /api/v1/capabilities and fails the release if a
feature is enabled with no routes behind it, which is exactly the state
that shipped.

The integration database is pinned to the engine and major version the
deployment runs. It was MariaDB 10.5 against a MySQL 8.4 plan; the
migrations use MariaDB syntax MySQL rejects outright, so a test database
on the other engine would have proved nothing about the release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The product shipped an ultramarine identity that belonged to no other
Bitcoin Universe surface. Core, Wallet and Inscribe are all built on the
same pink-forward system, so the explorer read as a different company's
product wearing our name.

This replaces the brand layer rather than recolouring it. Three roles now
exist where one did: a working pink that is readable both as text and as a
fill, the #ff0066 identity anchor for borders, rings and display type where
it carries no small label, and the lavender that carries everything the
brand pink should not, including focus. Surfaces move to pearl on light and
to the plum-black the rest of Universe uses on dark.

Nothing pink means anything. The five evidence states keep the conventional
green, amber, blue and red, protocol identity keeps its own scale, and the
fee scale is untouched, because a fee band means a fee rate.

Every value here was derived against the contrast module rather than picked:
the anchor is 3.85:1 on white, which is why it is not a button fill, and the
chart series were solved for separation under protanopia, deuteranopia and
tritanopia as well as normal vision.

The gate grew to match. It now measures that the brand is never a state or a
protocol, that two protocols are never the same colour, that dark and high
contrast declare every token light does, and that the retired ultramarine
values cannot come back. Adding it found three collisions that predate this
change: ordinals orange sat 11.1 dE from the amber meaning partial evidence,
and stamps and op data sat inside the new brand family.

331 palette pairings checked, 0 failing.
The page put two chips of equal weight beside each protocol: what the
registry says it implements, and what the runtime snapshot says its
authority can do. So Ordinals, Rare Sats and Runes read "Live, read only"
next to "Authority unreachable", and the summary counted all three among
the protocols readable that day. Both facts were true; neither was the
answer to the question a reader is asking.

Availability now decides the primary label and the count. A protocol whose
authority is unreachable, unconfigured, or still catching up is not
readable, whatever the registry says it implements. The registry capability
follows as a qualifier, and a line underneath gives the evidence: where the
authority has reached, how far behind that is, how many checks have failed
in a row, and when it was last asked.

An authority that is catching up gets its own state rather than being
rounded to working or broken, because that is what an index rebuild
actually is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing zero

A currency with no rate fell through to a multiplier of zero, so every
amount rendered as a confident $0.00. That is the same error as answering a
failed query with an empty list: it turns "we do not know" into a definite
value, and a reader has no way to tell the two apart.

With no usable rate the amount is left blank and titled. This surfaced when
the third-party price feed was switched off, but the fallback was always
there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the docs

Search is the product's dominant action and now looks like it: the brand
sweep, a top gloss, and a soft glow, on the one control allowed to carry
them. Hover moves brightness rather than swapping to a second gradient, so
the measured relationship between the fill and its label survives.

The gloss is 0.16 rather than the 0.24 that looked better. At 0.24 a white
label falls to 4.06:1 on the violet end of the sweep, which the gate now
measures: the label is checked against every gradient stop, through the
gloss layer, in all three themes.

The research this came from is recorded rather than asserted. A visual
language pass read each competitor's declared shell colour out of its own
markup and found the category uniform: mempool.space at #1d1f31,
Blockchain.com black, Blockstream dark, and a documented convention of low
chroma and cool temperature. Light, warm and chromatic is an empty position,
and the pass checked whether it is empty for a good reason before taking it.

Two further passes on different strategies produced no new competitor,
capability class, or trend, which is the closure condition.

343 palette pairings checked, 0 failing.
The check rendered LF and read a working copy that this Windows host checks
out as CRLF, so it reported a clean tree as stale and could only ever pass on
the Linux runner. Competitor measurements move to the research doc, where the
branding and origin gates already expect to find a competitor named.
The visual matrix checked overflow, console errors, contrast and
accessibility. A Charts page that never resolved and a Mining dashboard of
35 skeletons passed all four and shipped: nothing overflowed, nothing
logged, placeholders have fine contrast, and axe is content with a
skeleton.

The matrix now measures whether a page finished. With a populated fixture
there is no excuse for a loader still on screen after the settle wait, for
a skeleton that never resolved, or for a chart panel that drew nothing, and
each of those fails the run. With a failure fixture the obligation is the
opposite: a page still waiting must have said why.

Fixtures still cannot prove the deployment can produce data at all, so
synthetic-check.mjs asks the live origin directly. It fails on a feature
advertised with no routes behind it, a range that answers with nothing, a
protocol marked readable whose authority cannot answer, a configured
authority with no checkpoint, and a frontend and backend on different
builds. It runs after a release and hourly.

The backend integration tests now run in CI too, against a real database,
because the migration failure that had to be found by hand was a database
one and no test in the suite touched a database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deployment notes described a database that was off, and the release
procedure was a list of manual steps with no gates. Both are now what the
machine does: a dedicated MariaDB container, why it is MariaDB and not the
MySQL pinned elsewhere, the bounded indexing window and why it is bounded,
the bundled pool metadata, the price feed that is off and why, and a
release that refuses to cut over on a configuration it cannot serve.

The overlay ADR gains the status contract. Capability and availability were
never written down as separate things, which is how they came to be
rendered side by side with equal weight and read as one answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a reason

Three defects the pink found. A loud colour is useful that way: it shows you
where the hierarchy was already broken.

The range selector painted all eleven periods as filled primary buttons and
marked the chosen one with .active alone, so nothing on screen answered
"which period am I looking at". Inside a group of alternatives the unpicked
options now go quiet and the fill means "this is the one". Standalone primary
buttons are untouched, because those are actions rather than answers. This
also fixes the Mempool and Mining switcher and the RBF filter.

The label above a number on a stat card took the brand colour, on 45 of them,
which made the mining dashboard nine pink labels competing with the figures
they introduce. Brand marks identity and intent; a label is neither.

The largest mining pool was drawn in #D81B60, 6.7 dE from the brand pink, so
a pool's identity wore the product's colour. That value and #880E4F are now
held back from the pool ramp alongside the yellow already reserved for the
unknown pool.

Two more things the review turned up. The chart zoom slider was drawn from
the charting library's defaults, a pale blue measuring about 1.15:1 against
the page, on eighteen charts; it is themed once now and spread from there.
And the error detail under a failed panel read "(undefined undefined: )" for
any failure that was not an HTTP response, because the pipe read .status off
whatever it was handed and its null guard came after the property access.
It now answers with something actionable or with nothing.

154 frontend tests pass, 343 palette pairings, 0 failing.
The capability probes and the bundled pool import each catch their own
errors and return a value rather than throwing, so awaiting them is safe.
The lint rule cannot see that without the annotation, and it was right to
ask: the rollback in the pool import was itself an unguarded await, so a
failing rollback would have replaced the error that caused it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mempool depth chart drew its fee bands from the inherited categorical
ramp, which is a rainbow. Applying a categorical palette to an ordered
quantity means the colour tells a reader nothing about whether a band is
cheap or expensive, and one of its bands sat 6.7 dE from the brand pink.

Blocks and the Lens already read this product's fee scale. The depth chart
and its legend now read the same one, so a fee rate has a single colour
everywhere: green for cheap, through amber, to deep magenta for the most
expensive band. The scale is a TypeScript array rather than a custom
property, so the theme is detected from a variable only the high contrast
theme declares, and the answer is cached and dropped on a theme change like
the rest of the chart chrome.

None of this was visible before, because the visual QA fixture reported a
mempool of roughly eighteen thousand vBytes, four orders of magnitude below a
real one. Every y axis label rounded to "0 MvB" and the bands were too small
to resolve, so the most colour-dense surface in the product rendered as one
flat area and its palette was never actually reviewed. The shape is
unchanged; only the scale is real now.

One pairing in the product does not clear the 25 dE the brand is held to
everywhere else: the top of the fee scale sits 10.7 dE from the light brand
fill. Both values are load bearing and neither can move, so it is recorded in
the gate at the distance it actually has. Nudging either one closer now fails
the build.

346 palette pairings, 0 failing. 154 frontend tests.
A gate that never fires is indistinguishable from no gate. The judgment is
exported and covered directly: a spinner still turning on a populated page,
skeletons that never resolved, a chart panel that drew nothing, a page that
is only placeholders, and a failure state that waits without saying why all
fail; a page that finished and a failure state that explains itself do not.

Importing the harness no longer launches it, so the test can reach the
judgment without driving a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clock link, the fee-level filter, and the invert toggle are icons with
a title and no accessible name, so a keyboard or screen-reader user reaches
three stops that announce nothing. The keyboard harness had been reporting
all three; nothing acted on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI runs it hourly, which only covers the window CI happens to be awake. A
timer on the machine that serves the origin covers the rest, writes to the
journal, and marks the unit failed when a check fails, so an outage between
releases is visible where every other service failure already is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… fill rule

The source page exists to say which commit is running. It asks for that over
REST, and that route had no fixture, so the one page whose whole job is to
publish the release rendered with an empty release and an empty backend in
every screenshot of the matrix. Nobody reviewing it could have seen whether it
works.

The design system now also states the distinction the review kept running
into: filling a bar is not carrying a quantity. A progress bar or a
single-series area may be brand pink, because the length carries the value and
the fill is only the mark. Colour that encodes the value stays off limits. If
changing the number would change the colour, the colour cannot be the brand.
Fulcrum is gone from the explorer host: no process, no data directory, no
unit file. The backend was still configured to reach it, and a dead address
backend does not fail loudly, it retries. It was producing roughly two
connection errors a second, forever, which buried every real error in the
journal.

Core answers blocks, transactions and the mempool either way, so the
deployment reads addresses from Core for now and an address lookup fails
immediately rather than hanging. The preflight refuses an electrum backend
when nothing is listening on its port, so this cannot come back quietly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s room

Two pages carried a 39 KB decorative block graphic beside their heading, one
on the blocks list and one on the transaction test page. Both were drawn for
a dark shell with colours from no palette in this product: navy, charcoal and
an amber gradient sitting on a pearl page. They carry no information, and a
floating decorative shape is the thing this design system exists to avoid.

Removing them also removes 39 of the 93 colour literals the branding
allowlist was carrying, and takes the same weight out of the sprite sheet
that ships in the main bundle.

The block size bar's label is positioned out of flow, so it landed on
whatever followed it. In the blocks table that was the next row, and the
figure was cut in half. The reserve is made only where a label exists.
Five rules used --info, which resolves to the evidence state that means
"informational", as a generic accent. That is what the theme this product
grew from did, because that theme was blue and the accent happened to match.
Under a pink brand they were the only cool text left, and the API docs page in
particular rendered every endpoint header in the colour that means a status
everywhere else.

An endpoint header and a collapsed disclosure are controls, so they take the
brand. A subtitle and a code label are structure, so they take secondary text.
A hover is interaction, so it takes the brand.

The three genuine uses stay: the duplicate transaction alert, the informational
badge, and the proof widget's active marker are all actually informational.
…at it says

The new visual gate found this on its first real run: with every request
held open, the protocols page sat on its skeleton indefinitely. The registry
read had a catchError but no deadline, so a request that hangs rather than
fails left the page waiting with nothing subscribed to clear it. It has a
budget now, and the error state it reaches offers a retry.

The gate's own rule was also wrong for the fixture named loading, which
holds requests open on purpose to photograph the waiting state. Asking that
to have finished asks the wrong question. It is judged on whether the wait
is announced at all, which is what a screen reader needs and what a blank
rectangle fails; the deadline itself is covered by the lifecycle tests,
which run far longer than the harness waits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A run pointed at a second output directory committed 21 screenshots and its
report, because only the default directory was ignored. Ignore any output
directory the harness is pointed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statistics are collected from the moment the writer starts, and nothing
backfills them: there is no first-party source to backfill from, and
inventing rows would be worse than having none. So a 1W range currently
draws a couple of hours of samples under a heading that says 1W.

That is not a lie the chart tells on purpose, but it is one a reader would
take away. When the samples cover noticeably less than the range asked for,
the page says when collection began and that this is all the history there
is. The note disappears on its own as the history fills in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The history note read "1 hours ago", and it was a getter, so it reduced
over the whole series on every change detection pass, on a page that takes
a live sample every minute. It is computed when the series changes now, and
it counts in singulars.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The matrix covers three themes, seven widths, and six data states. It does not
cover forced colours or 200 percent zoom, and both are things this product
claims to support.

Forced colours is the interesting one, because the brand does not survive it
and is not supposed to. The operating system replaces the palette wholesale,
so the question is whether the interface still works once every colour
decision in this repository has been overruled: are controls still bounded, is
text still present, is anything painted on itself. That is a different failure
mode from low contrast and nothing here was looking for it.

Both hold on every route checked, with no horizontal scrolling at 200 percent,
which at a 1280 viewport is a 640 pixel layout.

The socket is not mocked in this check, so the chain strip shows placeholders
and the header reports itself offline. That is stated in the file rather than
left for someone to rediscover: the questions it asks are answered by page
structure, and the data surfaces are the matrix's job.
…it hide

The mining dashboard's hashrate and difficulty panels had no fixture, so half
of one of the thirteen reviewed routes rendered as skeletons and a spinner in
every screenshot the matrix has ever taken. Giving them data made the panel
render, and the panel immediately failed: the difficulty change figure is
#42B747 on the light page, which measures 2.37:1 against the 4.5:1 it owes.

The colour is set through an Angular property binding,
[style]="up ? 'color: #42B747' : 'color: #B74242'". The colour gate's markup
pattern required a literal style attribute, so a binding that paints exactly
the same way was invisible to it, and had been for as long as the gate has
existed. The pattern now matches [style], [ngStyle] and [attr.*] too.

Green still means up and red still means down. They are the evidence state
tokens now, which are the versions that clear the floor on every surface.

Mining now reports 0 contrast failures and 0 accessibility violations across
light, dark and high contrast, where before it reported nothing at all.
A restore was verified by hand, which proves the dump format works and
nothing else: there was no backup being taken. A daily timer now writes to
the same directory and keeps two weeks. It refuses to keep a dump that is
suspiciously small and checks the archive reads back, because a backup
nobody verifies is not a backup.

The deployment notes gain the measured growth figures, so the next person
deciding whether to prune or to widen the indexing window has the numbers
rather than a guess. At roughly 300 MB a year against 1.5 TB free, nothing
is pruned, and statistics are kept indefinitely on purpose: the "all" range
is the whole series.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notes still told the reader to compose a dump command by hand, which
was accurate before there was a timer taking one every day. They now name
the unit, and they say to verify a restore rather than to trust that a dump
exists, because that is the part that was actually missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bitcoinuniverseadmin and others added 14 commits August 28, 2026 12:19
… exist

check-palettes proves the token pairs are readable: white on the brand fill,
near black on the dark theme's brand. It cannot see whether a component
actually uses them together, and a rule that paints a brand background and
says nothing about colour inherits the page ink, which on light is near black
on hot pink. That is the exact failure the brand and contrast tokens were
split to prevent.

Five surfaces had it: the accelerate button and the four menu tier badges.
None of them render in this deployment, because both features are disabled
here, so no screenshot could ever have shown it and no reviewer could have
caught it. They would have been wrong the day someone turned them on.

The check only looks at rules that also lay out text, so a dot, a bar, a rule
and a toggle track are correctly left alone. 62 fills checked, 0 failing.
The step failed in CI on its first run: a container left behind by an
earlier run was still up, so compose reported it as already running and
never recreated it. The engine had changed underneath it, and the readiness
probe then waited out its attempts on a database that was never going to be
the one the tests asked for.

Tear the previous container down, volumes included, and recreate. Remove a
stray container holding the same name too, because `down` only removes what
compose owns and `up` fails on the conflict rather than starting anything.
Probe with either client binary name, since MariaDB renamed them and a probe
that knows one name reports a healthy server as a database that never
started.

Verified by leaving a container of the wrong engine running and watching the
suite replace it and pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The em dash gate scanned the repository and skipped the build output, which is
the half a reader actually sees. Copy reaches the bundle from templates, from
generated configuration and from metadata, so a clean source tree was never
proof of a clean page.

Pointed at frontend/dist it passes today, with one exemption: the third-party
licence notice reproduces every dependency's licence exactly as its authors
wrote it, and editing somebody's licence to satisfy our punctuation is not an
option. It is the only file in the output carrying the character.
…nshot

The fixture pinned only the address summary. The harness falls back to a
prefix match, so /api/address/<addr>/txs matched the summary's key and was
answered with the summary object. The page called forEach on it and threw. One
of the thirteen reviewed routes has been rendering its error state in every
screenshot the matrix has ever taken, and the error it rendered was the
"(undefined undefined: )" this branch already fixed.

Pinning the sub-routes made the page render, and the page immediately failed
contrast: the negative balance button is .btn-danger, which the Bootstrap
bridge never restated, so it kept the framework's own #dc3545 and inherited
the page ink at 3.21:1. .btn-warning and .btn-info had the same gap. All three
now take the state tokens and declare their ink, which also fixes the stale
block badge, the unconfirmed count, and the replaced and removed states.

check-fills cannot see this class: the fill comes from compiled framework CSS
rather than from a token in our own stylesheets. Measuring rendered pixels is
what caught it, and that only works when the page renders.

Address, block and transaction now report 0 contrast failures and 0
accessibility violations across light, dark and high contrast.
The production monitor recorded the last deploy as an outage: three checks
failed at the exact second of the cutover. The backend and the overlay take
a few seconds to listen again after a restart, and the gateway answered 502
the instant the connection was refused, so every request in that window
became a visible failure.

A request with no body is now retried over about five seconds while the
connection is refused. That bridges a restart, and it never retries a write,
because only GET and HEAD can be replayed. An upstream that is genuinely
gone still gets a 502, still well inside the page's own budget, so a real
outage is reported rather than hidden behind a long wait.

Verified by starting the gateway against an upstream that is not listening
and bringing it up a second later: 200 after 1.8s, against 502 after 5.3s
when nothing comes back at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Probing the origin through a cutover still caught two 502s. The retry added
for a restarting upstream cannot help there: the gateway itself was being
restarted, and nothing behind a process that is down can answer for it.

It does not need restarting for most releases. The backend and the overlay
run from a path baked into their unit at exec time, so they have to come
back; the gateway resolves its static root per request, so a new frontend
reaches it through the symlink on the next request. It restarts only when
its own file differs from the running release, which is the one case that
still shows a brief gap, and the log says so when it happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The notes said a deploy caused at most a brief connection reset, which was
a guess. It has been measured now: probing the origin once a second through
a cutover returned 200 for every request. The one case that still shows a
gap is a release that changes the gateway itself, and the notes say which
case that is rather than leaving the reader to find out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It read the page with an evaluate that could be in flight when the router
redirected, which destroys the execution context underneath it. A race, so it
passed here every time and failed on the runner.

Zoom is applied through an init script now, so it lands on every document as
it is created and nothing has to be evaluated after a navigation that may
still be running. The measurement waits for the app to mount, and retries once
if the context goes away underneath it. A second failure is a real one.
The rule styling a result group's label declared font-size twice and the
second one won, so "Transaction" rendered at the same size as the transaction
it was introducing. The grouping did no work: every line in the dropdown
looked like a result.

It takes the eyebrow register now, which is the one place in this design
system where small caps and wide tracking are allowed. The results lead, the
labels sit above them, and the dropdown is shorter for it.

Search recognises the identifier classes it should, keyboard selection walks
the list, and the selected row takes the brand tint. Checked against a
transaction id, an address, a height, a prefix, and a string that is none of
those, in both themes.
…t of the system

The file index named three gates and the matrix. There are seven gates and two
harnesses, and four of them were added by the work that found the defects they
now prevent.

The note on fixtures is the more important half. Five defects on this product
were invisible for as long as they existed because the surface that showed
them had no fixture and rendered as a skeleton, an error, or a flat shape in
every screenshot ever taken. A route that renders nothing passes every
automated check, so a missing fixture does not weaken a review. It removes it,
and reports success.
The gate failed its first honest run against every route, and each failure
was the gate being wrong rather than the page.

It probed at a fixed pause, which is a race and not a deadline: on a loaded
CI machine a page that finishes perfectly well is still mid-render at 2.6
seconds. It now waits for the page to settle, up to fifteen seconds, and
what fails a run is a page that never settles.

It counted a spinner nobody can see. The block overview keeps its loader in
the tree and fades the wrapper with opacity, so checkVisibility is used now,
which walks the ancestors and understands opacity.

It counted app-loading-indicator, which is a labelled progress banner and is
supposed to stay for as long as the work runs. Failing a release for saying
"Indexing blocks" is the opposite of the point.

It counted placeholders below the fold. Angular defers work until it scrolls
into view, so a block page holds a transaction placeholder there on purpose.
The harness judges a viewport, so the probe does too.

All thirteen routes pass now, and the judge's own tests still hold it to
firing on the states that shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run across every route and data state, the gate found four more pages with
the same fault: home and blocks hold a placeholder with nothing said about
why when the chain backend is down, and the transaction and address pages
render bare skeletons with no accessible announcement, so a screen reader
user hears nothing while they wait.

Those are real and were found here, but they are not what this change set
reviewed, and quietly widening the blast radius to make a gate green is how
gates end up disabled. The gate blocks on the three routes whose request
lifecycle has been rebuilt, and prints the rest every run under their own
heading, never suppressed. Adding a route to GATED_ROUTES is how that work
gets finished.

The loading rule also stopped demanding a spinner from pages that have
nothing to fetch. The docs and source pages render from the bundle, and
failing them for not waiting was the rule being wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three conflicts, all import adjacency where both sides added lines in the same
place. Their subject is how these charts load; this branch's is how they are
drawn, so nothing contended.

The gitignore takes their wider artifacts-*/ pattern, which already covers the
directory this branch added. The statistics component no longer carries a zoom
slider after their rewrite, so it no longer needs the shared slider style; the
mempool graph it delegates to still does, and all nineteen chart components
that draw a slider still spread it.
…p it lying

The unfinished-page gate was failing the mining dashboard at three widths with
"2 skeletons never resolved". It was right: the hashrate and difficulty panels
had no fixture, so they waited forever. This branch had already pinned those,
and with them the gate goes from six blocking failures to none.

The block page reported the same fault at 1024 and only at 1024, and there the
gate was wrong. That section is behind Angular's @defer (on viewport), so it is
supposed to stay a placeholder until the reader scrolls to it. The probe allows
a 200px margin around the viewport while an IntersectionObserver uses none, so
at that one width it counted a placeholder Angular had correctly not replaced.

The capture now scrolls to the bottom and back before it judges. Deferred
content loads, the view returns to the top so screenshots are unchanged, and a
skeleton still showing afterwards is genuinely stuck rather than merely patient.

Two more fixture gaps went with it: the neighbouring block on the chain strip
was unanswered, and a block declaring three thousand transactions returned one,
so the list sat waiting for the rest of a page that never came.

45 screenshots across five routes, three widths and three themes: 0 unfinished
pages, 0 contrast failures, 0 accessibility violations, 0 overflow, 0 console
errors, 0 blank canvases.
@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit c38bb77 into develop Aug 28, 2026
5 of 12 checks passed
@bitcoinuniverseadmin

Copy link
Copy Markdown
Author

Coordination note: PR 6 is being driven to merge and deployment by the session that owns fix/universe-explorer-production-go-state, which gained three commits after your ae4ba8e merge: 867717a (home, blocks, tx and address join the gate), 8c9a539 (tracker stage contrast), 6d48139 (per-run CI port with an identity check on the server). After PR 6 reaches develop, merge the new develop into this branch to pick those up; the fixed-port serve step this branch still carries will otherwise keep colliding with concurrent runs on a shared host.

@bitcoinuniverseadmin
bitcoinuniverseadmin deleted the feat/universe-glam-pink branch August 29, 2026 08:46
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.

1 participant