Skip to content

0.7.17 - the commodity page, session debriefs, honest maps, and eight defects fixed - #28

Merged
peans99 merged 31 commits into
mainfrom
release/0.7.17
Aug 24, 2026
Merged

0.7.17 - the commodity page, session debriefs, honest maps, and eight defects fixed#28
peans99 merged 31 commits into
mainfrom
release/0.7.17

Conversation

@peans99

@peans99 peans99 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Integration branch for the 0.7 line: dev070 and codex/map-truth merged into one thing to test, per the release rule, rather than tagging the sum of two branches nobody ran together.

What is in it

From dev070 — commodity buy parsing (the last unmatched tag in 418 MB of logs), the commodity drill-down page with price/margin/counter charts, the party channel read for the Crew page, pinned preparation checklists, the actionable Now briefing, trade routes ranked by reliability, automatic price refresh behind a preference, the LAN read-only guard, and the CI check that fails a parser change which does not retire the cache.

From codex/map-truth — session debriefs and route replay, system maps separated from the jump network, the live location made findable, focused map views, compact map symbols with service badges, and Now cards that can be hidden.

Eight defects fixed from a review of main..dev070, each checked by putting the bug back and watching its test go red:

Fix Test
Chart key coloured by the unfiltered series, so a short line shifted every swatch ChartKeyTests
Fresh only emptied the routes table on caches with no quote timestamps UexRouteReliabilityTests
A failed briefing fetch stranded the last location's card and blocked retries PilotBriefingTests
Add-task button never re-enabled after a failed request ChecklistTests
Attachment kind decided against a catalogue that had not loaded ChecklistTests
Leaving the commodity page kept its subject, so a link back did nothing covered
dailyMarket summed counters with unequal history ChartKeyTests
Catalogue fill duplicated ~10k options per list see below

Merge conflicts, resolved

Three, all real. The routes empty state had been improved on both sides for different reasons — evidence on one, freshOnly on the other — so it now names whichever tickbox emptied the table instead of keeping one explanation and losing the other. Directory.Build.props went to 0.7.17, above both lines. README.md keeps exactly one release-notes section, since the workflow lifts only the one matching the tag.

Verification

  • Both suites green from C:\Quantumwake against the real install: 375 + 119 = 494 tests.
  • dailyMarket's new per-counter average was checked against the real UEX history for Iron, where the sample runs 2 to 8 to 4 to 2 counters across the span — the sum slopes downward purely as reporting thins.
  • A security review of the branch found no newly introduced vulnerabilities; LanGuard, the StarStrings extraction fence, the checklist URL gate and the frontend's DOM sinks were each traced and held.

Known gaps

  • One fix has no test. Jint settles an await by draining the job queue where it stands, so two callers of the catalogue fill can never overlap — the first fetch returns before the second call starts, and withholding the response to arrange it hangs the engine. Recorded in dom.js so nobody spends the day on it again.
  • No render check. Headless Chrome produced one screenshot and then stopped writing output entirely in that session, across fresh profiles, absolute paths and both headless modes. The chart maths was verified against real data instead, but the charts were not seen drawing.

🤖 Generated with Claude Code

peans99 and others added 30 commits August 23, 2026 09:08
The buy tag was routed all along; the pattern behind it only fitted a sale, so
every purchase fell out as an unmatched line - the last one in 418 MB of logs.

Buying is the same transaction written differently. The total is price rather
than amount, transactionMode is absent, and the quantity is centi-SCU, so a 320
SCU hold reads as 32,000 unless it is converted. All three now handled, with
Quantity normalised to SCU on both sides since everything downstream - the
timeline line, the Commodities table, the per-counter totals - says SCU.

Anchoring matters more than it looks: price also occurs inside
shopPricePerCentiSCU and amount inside unitAmount, both of which trail the real
field on their own line, so a mis-anchored match takes a wrong number rather
than failing loudly. A theory covers both decoys.

Verified against the install: 13 purchases parse, unmatched known tags drops to
zero, and each one's aUEC-per-SCU agrees with the shopPricePerCentiSCU the game
printed beside it - a field the parser does not read, so it is an independent
check on the hundredfold conversion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parser change is invisible without this. Backups are skipped by
fingerprint, so a session summarised before purchases parsed keeps that payload
for ever - and the installs with the most cargo history are exactly the ones
that would have kept showing zero.

PayloadVersion is folded into the fingerprint for this reason; bumping it to 4
retires every row and costs one cold backfill on the first launch after the
update. The remarks on the constant already describe this failure happening
once, with medical beds. Said plainly in the release notes too, since a slow
first start is worth warning about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prices are the only dataset here with a shelf life. Everything else is local or
effectively static; a price table pulled a fortnight ago looks exactly like one
pulled this morning while every margin computed from it is quietly wrong.

That makes this the first thing in the app with a reason to reach the network
unattended, so it is a preference rather than a default, modelled on the update
check: asked once, off until answered, revocable in Settings, and a refusal
recorded so nobody is asked twice. Offered in first-flight setup as a condition
of taking UEX at all, and cleared if UEX is unticked on the way out.

The guard that matters is the second one. A null fetch time means UEX is off,
and reading "never fetched" as "infinitely stale" would turn a switch labelled
keep my prices current into one that enables an integration the player had
declined, so IsDue answers no and keeps answering no until something else
enables UEX. Both conditions are re-read on each tick, so revoking either takes
effect without a restart.

A failed attempt still records the attempt - hence "last tried" rather than
"last fetched". Without it, stale prices plus an unreachable feed is a fetch on
every tick for as long as the app stays open. Checked() takes the clock for the
same reason IsDue does: the two are compared, so a test that can move one but
not the other cannot exercise the backoff at all.

The standing promise needed rewording, not quiet abandonment: README, About, the
FAQ, both dataset blocks and the release template all said the app never goes out
on its own, and that is now true only of everything except this and the version
check. Both are named, and both are off until turned on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There is no roster event, no join event carrying a member list, and no player
id anywhere. The HUD's party toasts are the whole of what can be known about
who you fly with, so the new Crew page is built from them and says so.

The channel is free text, which makes the risk reading a word out of a sentence
and calling it a person. Five titles appear and they are not interchangeable:
Party carries arrivals and departures, New Party Leader the handover, Party
Disbanded the end, and Party Launch / Party Launch Accepted are matchmaking
chatter naming a leader but saying nothing about who is present. Disbanding is
matched on its title because its body names nobody - reading it the obvious way
yields a player called "The". A handle must be one unbroken word for the same
reason, and everything unrecognised is left unread rather than guessed at.

The counts are ranked by sessions, not by toasts: one player with a poor
connection would otherwise outrank nine people who each flew a whole night. Your
own handles are dropped, since lead passing to you names you in your own logs.

What this cannot do is the part worth repeating, and the page leads with it:
somebody already online when you group up, who stays until you log off, produces
no toast at all. Every figure is a floor, never a total.

Verified against the install: 291 party notifications, 273 read, 27 people
named, 18 unread and all of them accounted for as queue chatter plus one line
the game garbled. The CLI prints both halves so that gap stays visible.

PayloadVersion goes to 5 - a new field on the summary, so cached sessions name
nobody and the page would be empty for everyone but new installs.

Also records the freight investigation as the dead end it turned out to be:
6,488 hits, and not one item class or quantity among them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Market answered "where does this sell" and stopped. The drill-down adds the two
questions a trader actually asks next - what has it been worth lately, and is
anyone still buying - plus both counter lists and this install's own receipts.

UEX turns out to serve per-counter price and stock history, which is where the
charts come from. It serves it strictly one counter at a time: id_terminal is
required, so a good trading at thirty-five counters is thirty-five requests to
draw one line. SampleTerminals asks the busiest few instead, taken from both
ends of the trade - most demand, where a hold empties, and most stock, where it
fills - and the page says how many of how many it sampled, since "the price of
Aluminum" and "the price at the six busiest counters" are different claims.

Ranked by volume, never by price. The best price is often a counter wanting
nine SCU, and a trend drawn from those describes a market nobody trades in.

The carry-forward in dailyMarket matters as much. Counters report when their
contributors look, so summing only those that reported on a given day makes
demand collapse and recover on the rhythm of UEX's volunteers rather than the
game's economy. Each counter holds its last figure until it reports again.

Fetched on the click that opens the page - never on a page load, never while
UEX is off - then cached, because opening the other chart should not spend the
requests twice. The page carries its subject in the fragment, so a drill-down
is a link like every other view and survives a refresh.

One trap worth naming: a local named "history" shadows window.history for the
whole function, so writing the fragment threw before anything rendered. The
local is "trend" now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The most expensive failure here is the one nothing goes red for. Backups are
skipped by fingerprint, so a session summarised before a field existed keeps
that summary for ever: the parser reads the new thing, the page asks for it,
and every install that has run before shows nothing. The installs with the most
history see the least.

Twice now - medical beds, then commodity purchases in this very branch, which I
had to fix in a follow-up commit. The release workflow already demonstrates the
remedy: its tag-versus-props check is what makes "always bump the version" a
rule rather than a habit. This is that, for the version nobody remembers.

A pull request touching the parser, the events, the session record or its
builder must move PayloadVersion. A rename or a comment fix changes nothing
stored, and a check that cannot be answered is one people learn to route
around, so no-payload-bump in a commit message waives it.

Replayed against this branch's own history before committing: it fails d4ac219
alone, which is exactly the mistake, passes once e995ad8 supplies the bump,
passes the party commit that carried both together, and stays quiet on the two
commits that touched no parser. The waiver was exercised on a synthetic commit.

Ubuntu, because it is git and grep and needs none of the SDK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CIG has stripped telemetry patch by patch, and from inside this app every one
of those removals was indistinguishable from an evening off. docs/findings.md
records the pattern; untapped-signals.md warns that anything built on these
should degrade to a labelled empty state and that the health panel should cover
it from day one. There was no health panel. This is it.

Read from stored sessions, not from the parser. That is the load-bearing
choice: a scan skips every unchanged backup, so parser counters describe
whichever files happened to be re-read rather than the install. Summaries are
the whole history and are what the pages already draw from, so this answers
"is the app still seeing this" rather than "did something match just now".

Quiet is measured against this install's own last session rather than against
today, so somebody returning from a month away is not told that everything
broke while they were gone. Zero and never are shown as such, with the reason
where one is known - kills carry the 4.9 note, deaths say they are inferred
from corpse bursts - because a bare zero reads as a broken feature rather than
a missing event, which is exactly why the kill counter was removed in 0.6.8.

Unscoped by wipe on purpose: a wipe ends an account, not the client's
willingness to log a thing, and drawing that line here would make a removed
event and a fresh start look identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard grepped every commit message in the range for a bare word, and the
commit that introduced it explains the waiver by name - so the check waived
itself the moment it was documented, and would have stayed off for the whole
0.7.0 pull request. A guard that fails open when someone writes about it is
worse than no guard, because it still looks green.

It is a trailer now, anchored to the start of a line and requiring a reason
after the colon. Prose can name it without invoking it, and "No-payload-bump:"
with nothing after it does not count either.

Replayed again: still fails d4ac219 alone, still passes once the bump lands and
on the party commit, now correctly refuses to be waived by a message that
merely mentions the trailer, accepts a real one, and rejects an empty one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three surfaces, three shapes, each chosen for the data it actually has.

Cargo gets running totals rather than per-week takings. A hold is sold a few
times a month, so a weekly line is mostly floor with occasional spikes, which
reads as a business collapsing between runs instead of one being run
occasionally. Cumulative only ever rises, and the gap between earned and spent
is the number a hauler wants.

The expanded Market row gets a sparkline: no axes and no grid, because at that
size a gridline is noise and the two numbers worth having - the range, and how
long it covers - read better as a sentence beside the line. A button next to it
opens the page, sat by the title rather than pushed right, since the detail
table is wider than the panel and anything pushed right leaves the screen.

The drill-down gets margin per SCU, drawn only where both ends of the trade are
known - subtracting from a missing buy price would redraw the sale price under
a name that means something else - and a line per counter, capped at the
palette, because past four the colours repeat and it stops being readable.

Also un-hides the drill arrow on Market rows. Revealing it on hover made it
findable only by accident and not at all on a touch screen; it is dim now
rather than absent.

Verified by rendering each: cumulative earnings against spend on Cargo, the
strip reading 3,500-4,600 aUEC over 38 days on Market, and margin plus three
counter lines on the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build mutated the totals it was reporting. Closing the open stretch called
Accrue, and nothing advanced the marker it measured from, so each rebuild added
the whole span again. Harmless for a backup consumed and built once; ruinous
live, where Snapshot rebuilds on every event and every two-second broadcast and
the result is archived on rotation - an evening saved with a playtime in weeks.
Build reads state without changing it now, and a test rebuilds a hundred times
and expects the same answer.

-Lan bound every interface with no login on anything behind it, including the
endpoints that store UEX credentials and write into the game directory. The
purpose of -Lan is a tablet showing the dashboard, which needs reads and the
live feed, so off-machine requests are now read-only: GET, HEAD, OPTIONS and
the broadcast-only hub pass, everything else gets a 403, and startup says so.

TradeDataRefresh could take the host down. Checked() writes a file and sat
outside the try, and the catch filter covered network faults but not disk ones,
while EnableAsync writes eight files. An unhandled background-service exception
stops the host by default, so a full disk would have closed the dashboard, the
overlay and the live feed because a price refresh failed. Nothing escapes now;
unexpected types are logged as warnings rather than swallowed silently.

Disable could be undone by a refresh already in flight - reachable since 0.7.0
made refreshes automatic. The fetch stays outside the lock, the commit moved
inside it, and a generation taken at the start makes a fetch that finishes
after a Disable stand down. Verified by removing the guard and watching the new
test fail. The history cache is guarded by the same lock, since two readers can
now open a commodity at once.

StarStrings recorded a file as displaced only after extracting it, so a failure
partway through a file already being overwritten left it out of the rollback,
along with its backup. Recorded before the write instead. A manifest that
cannot be written now undoes the install rather than leaving a modded game
folder the app has no memory of touching.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md covered where the repo is and how to see the UI. It did not cover the
three things that have actually gone wrong, all of which fail without anything
turning red.

PayloadVersion gets its own section, because forgetting it has shipped a broken
feature twice and the symptom is that the installs with the most history see the
least. The release-notes rule gains the half that bit us: the workflow lifts
only the section matching the tag, so anything filed under another heading ships
unmentioned. And running the CLI over the real backups is now a step of its own
rather than an implication - green fixtures prove the fixtures still parse, not
that the app reads this install - with the point that the strongest check is one
the code did not use.

Also records the UI traps found by actually looking: virtual time stalling
behind the live stream so timers never fire, tables discarding a panel when
their data lands, and the three defects screenshots caught that a diff would
not have.

AGENTS.md is new, for agents that look for that name. It defers to CLAUDE.md
rather than restating it, and carries only the short list of what goes wrong
most often - two documents saying the same thing in different words is how they
start disagreeing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The LAN rule and the StarStrings rollback shipped without tests last time, which
is a poor way to leave a security boundary and a routine that writes into
somebody's game folder.

The LAN decision moves out of the middleware into LanGuard, where it can be
asked questions without a host. Extracting it found a hole: the inline version
used StartsWithSegments, but writing the same rule by hand invites StartsWith,
which admits /hubbub and /hub-admin/install. The rule is stated by method rather
than by a list of paths on purpose - a deny-list of sensitive endpoints is a
list somebody forgets to add to, and everything that changes something here is a
POST, including whatever gets added next week.

StarStrings gets a real archive, a real game folder and a served release, so an
install can be run and undone end to end. The manifest-failure path is exercised
by putting a directory where the store wants to write its json. Checked the same
way as the UEX race: guard removed, test fails, guard restored.

Tests now reference the server project, for the policies that live there rather
than in a store.

Versioning: patch goes up with every change from here, in the same commit as the
work, with the release-notes heading renamed to match rather than a second one
opened - so there is always exactly one section and the workflow can always find
it. Major and minor move only when Nicolas says. Written into CLAUDE.md and
AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Landed on top of Codex's 0.7.5, so this is 0.7.6; the work below was written as
0.7.2 before that branch appeared.

Expanding a Market row had started costing UEX up to eight requests. The strip
went in on the assumption it was as cheap as the row it sits under, and that row
used to read the local matrix and nothing else; comparing twenty commodities
would have been a hundred and sixty requests at a volunteer-run API, from the
same branch that set a six-hour refresh interval on the grounds of being polite
to it. The strip asks for one counter per side now, and the endpoint takes the
sample size and clamps it rather than trusting it.

That needed the history cache to remember what size it was taken at, or the
strip's thin answer would be served to the page that asked for four. A cached
sample now answers anything no larger than itself, so opening the page first
makes the strip free and opening the strip first does not spoil the page.

Its caption reads what came back rather than what was asked for, since a wider
sample already cached is reused and the line then really is a best-of.

HistoryAsync was the one place the generation guard had not reached: a Disable
landing mid-fetch left a price history cached for an integration that had been
turned off. Guarded like the rest.

/api/uex/enable said enabled:true unconditionally, including when the fetch had
stood down because Disable superseded it - so the page would show an
integration that is off as on. It reports what is actually the case now, and
the refresher no longer logs "refreshed 0 prices" for a deliberate no-op.

The lock's comment claimed readers see one table or the other. Per field that
is true; across two it is not, and ItemMarket reads two. Says so now.

Both new tests were checked by removing the guard. The first attempt at the
cache one was vacuous - the fake feed served no history points, so Sampled was
zero whatever the code did, and it passed with the guard deleted. The feed
returns real points now and it fails as it should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No-payload-bump: map presentation and web controls only; no stored data changed
No-payload-bump: map label presentation only; no stored data changed
No-payload-bump: map presentation, client filters, and test clock only; no stored data changed
No-payload-bump: map icon presentation and web tests only; no stored data changed
No-payload-bump: dashboard wording and tests only; nothing new is read or stored.
Eight findings from reviewing main..dev070, each one checked by putting the bug
back and watching its test go red.

The chart key coloured by position in the series it was handed, the chart by
position in the series it actually drew, so any line too short to draw shifted
every swatch onto the line above it. A player who has bought cargo but sold
none is the ordinary way to see it: the buy line draws cyan and the key calls
it orange. Both go through drawableSeries now, which is the whole point of
extracting it.

Fresh only emptied the routes table on any cache written before UEX's
date_modified was stored - every row unstamped, every route "unknown", every
one dropped - and the empty state blamed the location for it. The filter is
honoured only where there is a timestamp to honour it with, and the message
names the filter when one is on.

A failed briefing fetch left the previous location's stops, shopping and stash
on screen labelled with the new place, and blocked every retry until the player
travelled somewhere else entirely: the key was claimed before the request and
never given back. Claimed still, given back on failure.

The Now briefing aside, three smaller ones in the checklist composer. The
catalogue fill guarded on "already filled?" before its own await, so three
lists meant three fetches and three copies of ten thousand options - the exact
duplication the function exists to prevent. Add task disabled its button
outside any finally, so a dropped connection left the form dead until a reload.
And an attachment's kind was decided against a catalogue that had not loaded
yet, filing a real commodity as an item and sending the link into Parts search.

Leaving the commodity page kept its subject in openCommodityName, so a later
link back to the same commodity matched, returned, and moved the fragment with
Market still on screen.

dailyMarket summed demand and stock across counters with unequal history. Went
looking for the honest fix and the install decided it: on Iron the sample runs
two counters, then eight, then four, then two, so clipping to the days all of
them cover would have ended the chart on 08-07 and thrown away the last
fortnight, while the sum slopes downward purely as reporting thins. It reports
per reporting counter now, and the labels say so.

Also pinned the clock in the cargo window test. It measured three days back
from now against fixed fixture dates and flipped from 2 to 1 at 10:00 UTC
today; confirmed red at 55431c3 before touching it.

One fix has no test. Jint settles an await by draining the job queue where it
stands, so two callers of the catalogue fill can never overlap - the first
fetch has returned before the second call starts, and withholding the response
to arrange it hangs the engine instead. Written into dom.js so the next person
does not spend the day.

No-payload-bump: nothing stored changed - charts, a filter and a composer

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two lines of work that both bumped their own patch and both rewrote the same
three files, merged into one thing to test rather than a pair of branches whose
sum nobody ran. dev070 had reached 0.7.7 and map-truth 0.7.16, so the release is
0.7.17 - above both, since either number alone would go backwards for somebody.

Three conflicts, all of them real.

The routes table's empty state had been improved on both sides for different
reasons: map-truth explains an evidence filter that hid every row, dev070
explains a fresh-only tick that did the same. Keeping either alone would tell
somebody the wrong reason their table is empty, so it names whichever tickbox
is responsible and falls back to the location and the UEX-is-off cases as
before.

README keeps one section, as the workflow only lifts the one matching the tag,
and a second heading is how a whole feature shipped unmentioned in 0.6.13. The
0.7.16 and 0.7.7 notes are now one 0.7.17 section, with the defect fixes added
to it - they correct features in this same section, none of which has been
tagged, so they read as what the release does rather than as a changelog of a
version nobody ran.

Both suites green from C:\Quantumwake against the real install: 375 and 119.

No-payload-bump: neither branch touched the parser, the events or the summary

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@peans99
peans99 merged commit 0d6c6fc into main Aug 24, 2026
2 checks passed
peans99 added a commit that referenced this pull request Aug 28, 2026
… defects fixed (#28)

dev070 and codex/map-truth integrated as release/0.7.17, tested together from the real folder: 375 + 119 tests green.
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