release: promote verified Bitcoin Explorer baseline - #9
Conversation
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>
Universe Glam: the pink brand system, and the gates that keep its roles apart
The inherited about routes only proxy a hosted mempool API. This deployment configures none, so a request to them could answer nothing but 500 after a doomed outbound attempt. They are now mounted only when that API is actually configured, which makes them 404 like every other route this deployment does not serve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every combination of thirteen routes, seven data states, three themes and seven widths is nineteen hundred page loads. That does not fit the job timeout, and one run this morning was killed at exactly sixty minutes. A gate that cannot finish protects nothing. The broad pass walks every route at a phone and a desktop width with real data, which is where overflow, broken images and a page that never finishes show up. The deep pass takes the gated routes through every failure state, every theme and the widths that change the layout, which is where a page that lies about why it is empty shows up. Measured at two and a half minutes and sixteen minutes against the built frontend. A failure fixture also no longer waits the full settle budget. It has nothing to fetch, so it reaches its terminal state almost at once, and waiting fifteen seconds on each of those was most of what made the sweep unaffordable. The harness now stops with a plain message when the server it is measuring goes away, instead of recording every remaining route as a broken page and sending the reader to look at the application. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rer-production-go-state
…odes check follows the port The failure settle budget is four seconds, but the dashboard said nothing until ten and the blocks list retried for six. The deep pass passed anyway only because the axe and contrast probes delay the settle loop past those deadlines, which is a machine-speed race, not a margin. The dashboard now speaks at five seconds and the blocks list reaches its terminal state inside four, so what the gate measures no longer depends on how fast the runner is. The forced-colours check still pointed at the fixed port the serve step no longer uses; it follows the per-run port now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chain note, the registry version line and its inner span took their colour from the legacy --transparent-fg. It is rgba(36,26,43,0.62), which composites to about #77717c on the page surface and measures 4.33:1 against the 4.5:1 normal-text floor. The rest of this component already used --u-text-muted, which clears it. Six text declarations move to the muted token. The two border declarations keep the translucent one, because a border is not held to the text floor. Found by the broad visual pass against a verified-fresh build. Worth saying how it stayed hidden: an earlier run measured a stale bundle and reported a different, non-existent failure instead. On this machine `pkill` does not kill node, so orphaned gateways accumulate and one of them keeps serving an old dist. Compare the styles hash the server returns with the one in dist/index.html before trusting a local visual result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My previous commit reverted adbe39a: the gateway port in the modes-check step went back to a literal, the blocks-list backoff went back to two seconds, and the dashboard first-data timeout went back to ten. None of that was intended and none of it was in the diff I meant to make. Cause, for the second time in this branch: this worktree and another share one branch. A commit made in the other worktree advances the shared ref, so HEAD here moves while the files here do not. `git add -A` then reads the old bytes as a deliberate change and commits the peer's work away. `git status` does not warn, because from its point of view the tree really has changed. Stage the files you edited by name in a shared-branch worktree. `git add -A` is not safe here, and neither is trusting a clean-looking status after a merge that reported nothing to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oduction-go-state Make the visual gate affordable, and stop the gateway dying under it
The consolidation that moved this out of the dashboard restored the ten second value that adbe39a had deliberately tightened. At ten seconds, whether the visual gate saw the timeout panel depended on how slowly the probes ran on that machine; five keeps the page's terminal state inside the gate's failure budget by design rather than by latency. The first socket payload normally lands within two seconds, and a late arrival still clears the panel, so the tighter deadline costs a healthy deployment nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
develop gained b314061: the live-feed deadline is restored to the five seconds PR 8 reviewed (the stream consolidation had brought back ten, which made the deep visual pass a machine-speed race on home chain-down). One line plus its rationale; 182 frontend unit tests pass. CI restarts on the new head; this PR now promotes b314061. |
The artifact a release installs was built by hand on a Windows workstation. That machine has two ways of handing you a stale build without saying so. A leftover gateway process keeps serving an old output directory, and pkill in Git Bash does not kill a Windows node process, so those accumulate. And a build step that fails leaves the previous output sitting where a reader expects the new one. Both have already produced a measurement that described a build nobody was running, once as a false regression and once as a real one that went unnoticed for a while. A runner starts from a clean checkout, so the artifact matches the commit it names. Dispatch it with the ref to build; it prints the resolved sha. It repeats the four bundle gates rather than trusting the CI run on the same commit, because that was a different build. node_modules and the compiled gbt module are not packed, since install hard-links them from the running release, which is only correct while the dependency tree is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…king Two fixes to the release artifact workflow I added in 078f5c9. CodeQL was right to fail it. The ref input meant that dispatching from the default branch would check out an arbitrary ref and run its install and build scripts while holding the default branch's Actions cache scope, which is the scope universe-ci.yml restores from. That is a real path from "run a build of my branch" to "poison the cache every other job here trusts", and it is worth more than the convenience. The workflow now builds whatever ref it was dispatched against, which is what this was always going to be used for: the file has to exist on the ref being built anyway, because a dispatch executes the workflow as it exists there. The Pack comment also promised a dependency check that was not written. The check belongs in release.sh install rather than in the workflow, because that is where both trees are visible: the artifact already carries the backend lock file, so install now compares it against the running release and refuses the hard link when it differs. Hard linking a stale node_modules into a release that asked for different packages gives you a backend that starts, imports the wrong versions, and fails somewhere unrelated later. Reported by mempool-3d in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend and the overlay restart behind the gateway, and the gateway bridges that gap. A restart of the gateway itself could not be bridged: the port went away with the process and the edge answered 502 until it came back. release.sh already limited that to releases which change gateway.mjs, which is honest but does not help, because this release changes gateway.mjs. The gateway now accepts a listening socket from systemd on fd 3 and falls back to opening its own port when there is no handover, so it runs the same way under a unit, in CI and from a shell. universe-explorer-gateway.socket owns the port and the service drop-in requires it, which is what makes a restart leave the port bound: connections arriving mid-restart queue in the kernel backlog instead of being refused. The pid check on the handover is the part worth reading twice. LISTEN_FDS and LISTEN_PID are inherited by child processes, so a child that trusted them would listen on a descriptor belonging to its parent. Covered, along with the no-handover and malformed-handover cases, in gateway.test.mjs. Adopting this on a gateway that is already up costs one brief interruption, because the running process holds the port without SO_REUSEPORT and the socket unit cannot bind underneath it. That is worth spending once here, where a gateway restart was going to interrupt things anyway. The cutover log now states which of the two cases applies rather than leaving it to be assumed. Three other universe services on this host are already socket activated, so this follows the existing pattern rather than introducing one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ervice PartOf propagates stop and restart from the named unit to the one declaring it. With PartOf=universe-explorer-gateway.service on the socket, restarting the gateway would have restarted the socket too: service stops, socket stops and drops whatever was queued in the backlog, socket rebinds, service starts. That is a shorter unbound window than before and it is still an unbound window, so the comment in the unit and the success line in release.sh would both have been claiming something the units did not deliver. docker.socket carries the same declaration, which is why restarting docker.service is known to bounce its socket. Removing it is the whole fix. The service keeps Requires and After, so starting the service still brings the socket up first, and restarting the service now genuinely leaves the port bound. What this gives up is that stopping the service alone no longer takes the port down, because the socket outlives it and the next connection starts the service again. A deliberate full stop means stopping both units, which is now written in the unit and in DEPLOYMENT.md rather than left to be discovered. Reported by mempool-3d in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The socket unit introduces a way for a rollback to be the thing that takes the site down. A release from before the handover opens port 8099 itself, so if systemd is holding that port, that gateway dies on bind with the address already in use. The rollback path is reached precisely when something has already gone wrong, which is the worst moment to find a second failure. cmd_rollback now checks whether the release it is rolling back to understands the handover, and disables the socket first when it does not. That check is the reason inheritedListenerFd is an exported name rather than a local one: it is greppable in a release directory. Also reformats the dependency guard added earlier. Both guards were written with backslash continuations that arrived collapsed onto one line, which parses and behaves identically but reads badly at 187 characters. Nested tests instead. While reformatting I clobbered the cutover logging block with the wrong replacement and restored it from origin; the block is unchanged. Tested by extracting the guard from this file rather than retyping it: an older target disables the socket, a newer one does not, and neither happens when the socket is not in use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…red at Two things found by using this stuff rather than reading it. synthetic-check took a positional origin while everything else in this directory takes --base. Passing --base was not rejected: it was read as the origin, and failed several requests later with a URL parse error naming a string nobody typed. It now accepts either spelling and rejects anything else outright. This runs on the verification path after a cutover, which is not where an operator should be debugging argument parsing. DEPLOYMENT.md now carries the measurement behind the socket activation claim rather than asserting it: on a spare port on this host, with a throwaway unit pair since removed, a full restart under a 20ms probe returned 200 on all 400 requests while the pid changed underneath. It also states the rollback hazard next to the rollback instructions, because release.sh handling it automatically is not a reason for the operator not to know about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The frontend job had an hour. The deep pass alone measured 48 minutes once the gated route list went from three routes to seven, and the job was killed three minutes from finishing, having already produced a green broad pass and every bundle gate. Nothing was wrong with the code; the budget was set when the pass was a third of its current size. A gate that cannot finish protects nothing, which is the reason this matrix was split into a broad pass and a deep pass in the first place. Setting the budget to the measured cost of the pass that actually runs, with room to grow before it needs revisiting again. Seven routes across every failure state, three themes and three widths is 441 page loads. If it grows again the answer is a matrix over routes rather than a larger number here, since wall clock is what makes a slow gate get skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The zoom sweep failed five routes with 29 to 49 pixels of horizontal overflow, deterministically, on both runs of the same commit. The pages are not at fault. The measurement is. It set zoom: 2 on the root element. That scales the layout box without changing what media queries see, so the desktop breakpoints stay active and the desktop layout is rendered into half the width. No reader can reach that state: at real 200 percent zoom the CSS viewport halves and the responsive rules answer to the smaller width. Measured on a page with one min-width: 992px rule holding a 900px element. Under zoom: 2 the media query still matched, the element was still displayed, and the document overflowed by 260 pixels. At a real 640 viewport the query did not match, the element was hidden as designed, and overflow was zero. So the sweep now uses a 640 by 450 viewport at device scale factor 2, which is what the browser does at 200 percent. Verified against the live origin with the same fixtures: all ten checks pass, zero overflow, zero text painted on itself. That is the deployed build, which does not carry this release's layout work, so the result is about the method rather than about the fix. Consistent with the route matrix, which measures document overflow the same way at 320, 768 and 1440 and reports none. A gate that fails a state the product cannot be in teaches people to ignore it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Release record, final. Production serves main f1d51eb: frontend, backend and RELEASE-SHA agree, overlay backend-apis-f6bf1104, all units plus the gateway socket active, journals clean. 16 of 16 synthetic checks pass on the public origin. Post-cutover visual pass: every counter zero across 13 routes at 375 and 1440, against 6 contrast failures and 12 axe places on the same origin before the cutover. Cutover cost one measured 3.1 second gap, the pre-agreed one-time socket adoption; a subsequent gateway restart on the live origin under the same probe refused zero connections across 534 probes with the pid replaced. Observed through block 964484 arriving and serving with full data. The edge favicon and robots hijack found in the sweep is fixed at the explorer docroot with the main-domain redirect untouched. Deferred follow-ups are recorded in this PR's threads: duplicate push-plus-PR heavy runs, the zoom-200 tx body-chars floor margin, DEPLOYMENT.md notes for the edge trap and the release-tool refresh rule, and CI coverage for direct develop pushes while no PR is open. develop and main are synchronized at f1d51eb locally and on origin. |
Outcome
Promotes the repaired and verified Universe Explorer Bitcoin baseline from develop to main before multi-chain implementation begins.
Evidence
Release gate
Do not deploy until this PR's full protected check set passes. Capture the current public release identity as the rollback target, then use the atomic release procedure and public synthetic check.