0.7.17 - the commodity page, session debriefs, honest maps, and eight defects fixed - #28
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration branch for the 0.7 line:
dev070andcodex/map-truthmerged 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:ChartKeyTestsFresh onlyemptied the routes table on caches with no quote timestampsUexRouteReliabilityTestsPilotBriefingTestsChecklistTestsChecklistTestsdailyMarketsummed counters with unequal historyChartKeyTestsMerge conflicts, resolved
Three, all real. The routes empty state had been improved on both sides for different reasons —
evidenceon one,freshOnlyon the other — so it now names whichever tickbox emptied the table instead of keeping one explanation and losing the other.Directory.Build.propswent to 0.7.17, above both lines.README.mdkeeps exactly one release-notes section, since the workflow lifts only the one matching the tag.Verification
C:\Quantumwakeagainst 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.Known gaps
awaitby 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 indom.jsso nobody spends the day on it again.🤖 Generated with Claude Code