fix(observability): the alert path was feeding itself - #143
Merged
Conversation
logError() records an ErrorEvent. recordError() calls reportOpsAlert the first time it sees a fingerprint. reportOpsAlert logged itself through logError. So every alert produced a NEW message -- "ops-alert: " prepended to the previous title -- which is a new fingerprint, which is a first sighting, which alerts again: ops-alert: New error: ops-alert: New error: ops-alert: New error: ... One unconfigured integration was enough to start it. A missing Axper API key logged once, and the loop turned that into 1,154 ErrorEvent rows and a server too busy to answer a login. Every row was the loop's own output, so the error log -- the thing you reach for when something is wrong -- was the least usable artifact in the system. Two guards. opsAlert.ts now logs through `logger`, which writes to stdout and stops, and says at the top that nothing in it may call logError; the two channel-failure paths inside it had the same bug and are fixed the same way. And recordError refuses to re-enter the alert path, so no future caller can reopen it from the other side. Measured on the demo after the fix: /app went from 2.6-3.7s to 0.02-0.04s, and ErrorEvent stays empty instead of filling with its own output. opsAlertLoop.test.ts pins both guards, and checks that the fix was not simply "stop alerting" -- the cheapest wrong answer would pass the first three assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`seed:demo --reset` truncates EVERY table, including the ones other seeders own. Run on its own it therefore deletes the CMS content and the roles and does not put them back -- so the storefront quietly loses its copy, hours later, with nothing in the log to explain it. That is exactly how it was noticed: somebody asked where the copy went. Two changes. `npm run seed:all` runs the three in dependency order, which is what anyone reseeding a demo actually wants. And --reset now names the seeders whose tables it just emptied, so running the parts by hand cannot silently leave holes. The warning replaces a stray empty block that was sitting at the end of the seed -- pre-existing, and a good spot for something that should have been there all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Up Board and Store Traffic were hardcoded onto the dashboard. Both are showroom-floor conventions -- a rotation only exists where staff take turns on a floor, a door counter only exists where there is a door to count. A wholesaler, a manufacturer or an online-only shop got two permanently-empty cards on the FIRST screen after login, and an empty traffic card does not read as "no counter here", it reads as "nobody came in". Both are modules now, defaulting OFF, gated on the dashboard and across all five API routes that serve them. When neither is on the dashboard says so and points at the settings screen, rather than rendering nothing. A migration turns each ON where there is evidence it is already in use -- rows in the table it drives -- so no existing deployment loses a feature it relies on by upgrading. Off-by-default is right for the next deployment; silently removing something from the last one is not. Also fixes what this uncovered: - The features map in the demo seed was never actually corrected. #142 added the assertKnownModules import and the call itself did not apply, so main has been shipping a dead import and the same four invalid keys. Now 18 real modules, verified against the registry: no invalid key survives. - moduleManifest.test.ts pinned the module set with equality, so adding one failed a guard about a refactor that happened months ago. It now asserts CONTAINS -- every pre-refactor module still present, none renamed, no default flipped -- which is the guarantee that was actually worth having. - The POS asked /api/warehouse/positions for a productId the handler did not support, so the register got 50 arbitrary rows; and it read storeLocation.name where the API returns a flattened locationName, so the loop skipped every row. Every cart line said "0 on hand here" against a showroom holding twenty. It also counted stock committed to other orders as sellable, which is how the same sofa gets sold twice -- there is a freeOnly filter now, sharing freePositionWhere() with the allocator so the register and the allocator cannot disagree about what is sellable. Verified both directions in the running app: on, the dashboard shows 93 entries today against 110 last year and a five-person rotation with live statuses; off, both sections are gone and the page says where to switch them on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two integrations answering the same question -- where is this delivery and when does it land -- for the two ways a delivery happens: our truck, or somebody else's. They share the conclusion that matters. Both end in markHandedOver(), which generates the invoice and recognises the sale, so whether the fact arrived from a geofence or a carrier's status callback changes nothing downstream. That seam is already built; these are two more ways to feed it. Scoped by what the owner actually wants: GPS position tied to deliveries, and no video. Explicitly out of scope are driver scorecards, harsh-braking alerts and idle-time league tables -- the default reason telematics gets bought, the reason drivers resent it, and a measurement of the wrong thing. The test applied throughout is whether a feature changes what somebody does: an ETA text does, a monthly braking score does not. Three gaps found in the current model. Vehicle has no device identity. CustomerAddress has no latitude or longitude at all, which blocks geofencing, distance and sequencing -- it is a prerequisite rather than a nice-to-have. DeliveryStop.actualArrival already exists and nothing sets it. And one structural blocker for carriers: DeliveryStop.deliveryRunId is non-null, so a stop requires a run which requires a Vehicle. A third-party delivery has neither and therefore cannot be a stop -- invisible to the dispatch board and every metric reading it. The recommendation is a nullable FK rather than synthetic vehicles per carrier, because a carrier is not a truck and every report that counts vehicles would start counting carriers. DeliveryZone.isThirdParty and carrierName already exist and are display-only, which is worth knowing before someone assumes they do something. The value ranking puts telling the customer first, because a furniture delivery that fails for want of somebody being home costs a whole truck slot twice, and an ETA message is the cheapest thing that reduces it. Cost-per-delivery is fourth and is the one most retailers never learn -- holt is unusually placed to answer it because it already holds the fee side in DeliveryZone. Retention and employee-notice are called out as decisions to make before devices are installed rather than after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retention and employee notice were written up as "two things to decide deliberately", which is the wrong frame: a decision gets made once and then lives in someone's memory. These are deployment facts, and CLAUDE.md 61-63 applies to them exactly as it does everywhere else. So the doc now carries a configuration table instead, covering every number in it -- provider, poll interval, retention window, geofence radius, how much warning a customer gets and on which channel, when dispatch hears a run is slipping, whether the customer sees the van at all, whether the resequencer even offers a suggestion, and which carrier serves which zone. A shop with two vans and a shop with a carrier in three states want different answers to all of them and neither should be editing code to get them. Two points kept and sharpened. Retention is the setting with a consequence outside the building, and a retention setting nothing enforces is worse than none -- it writes down that you delete data you are still holding. So it needs a scheduled purge that runs and records that it ran, dropping raw positions while keeping the derived per-stop metrics the costing actually needs. And the notice is a configured document with a per-staff acknowledgement, not a constant, because the wording varies by state. That is also what makes this an easy conversation with drivers: tracking the van for the customer's benefit sells itself, tracking the person does not, and the notice can only say so specifically if the deployment can edit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught this, which is the manifest doing its job: eleven models are seeded now and were still marked `todo`, so the coverage gate failed on STALE -- "claimed outstanding, came back populated". Lead, CustomerInteraction, Proposal, ProposalLineItem, GiftCardPreset, EmailTemplate, LabelTemplate, TradeTier, SalesGoal and UpBoardEntry. TrafficSnapshot was marked SKIPPED, with the reason that its columns carried one vendor's brand. They stopped doing so in #127, when axperStoreName became sourceStoreName -- so the reason had outlived the fact and nobody noticed, because a skip with a stale reason looks exactly like a skip with a good one. The demo seeds two years of it now, and the traffic API falls back to those rows whenever the live counter is unreachable. Seed coverage: 79 -> 93 seeded, 63 -> 53 outstanding. No tranche emptied, so SEED_TRANCHES is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
goetchstone
added a commit
that referenced
this pull request
Aug 26, 2026
The tripwire caught these the moment this branch rebased onto the merged #143, which is the whole reason it exists: the scrub was correct when written, and a merge from another branch put the name straight back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
goetchstone
added a commit
that referenced
this pull request
Aug 26, 2026
* fix(tests): remove a real person's wealth record from a public repo
app/__tests__/windfallImport.test.ts carried a genuine Windfall export row --
full name, personal email, customer code, net worth and a boat-owner flag. The
comment said it matched the real sample CSV, and it did. Windfall is a
wealth-screening service, so that row is a named private individual's finances,
published on a public repository since the initial commit.
Nothing in the test depended on any of it. The parser is being tested for
column-NAME resilience; every assertion passes on any well-formed row. The
values are invented now, and a comment says so plainly so nobody restores them
from the real CSV to reproduce something.
First of several -- the same pattern reaches other fixtures -- but this one is a
private individual rather than a business, so it goes on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: remove client identity from code and make its conventions config
The repo is public. Beyond the identity leak, three of these were the same
bug as CLAUDE.md 61-63: one deployment's naming compiled into matching logic,
so a second deployment silently mis-routes rather than failing.
- reportRouter matched `Saybrook_Home_*` / `SH_*` report filenames. The
discriminator was never the org name -- it is "has an org prefix at all",
which is what separates the org's inbound-items export from the vendor's
unprefixed standard report of the same name. Now ORDORITE_REPORT_PREFIX,
defaulting to any prefix. All 25 existing routing assertions pass unchanged;
re-hardcoding the prefix fails the 6 new ones.
- isReturnOrder hardcoded the store codes SB/GT/CH/BB/WS/RS. The vendor's
convention is the A/M suffix; the codes are ours. Now ORDORITE_STORE_CODES,
same permissive default.
- Staff emails, a customer's phone, a vendor's phone, and the buyer identity
in four parser fixtures replaced with invented values at the same widths --
the fixed-width column offsets are the parsers' contract.
3,442 unit tests pass; tsc clean.
* fix: scrub real client and personal data, add a tripwire to keep it out
The repo is public and carried one retailer's production data as test
fixtures. Removed: 19 real people's names (staff and customers, across 21
files), a customer's and a vendor's phone number, staff email local-parts,
the company's email domain, its three store towns, and four real customer
street addresses. Replaced with invented values at the same shapes -- the
fixed-width column offsets are the parsers' contract.
Two guards were the same bug rather than only a leak:
- The seed's target check was a BLOCKLIST of database names. It failed open:
any name nobody had listed seeded silently, and the list only ever grew
after someone lost data. It is now an allowlist -- only a name that says it
exists to be seeded (seed/demo/scratch/sandbox/sample) runs unattended.
env.example's default moves holt_dev -> holt_demo to match, and setup.sh
uses the same rule so it fails before migrating rather than after.
This had no test at all; __tests__/seedTargetGuard.test.ts now covers it,
and the cases that matter are the unfamiliar names.
- restore-drill.sh and apply-preset already gated on allowlists, so their
deny-lists were redundant as well as identifying.
__tests__/clientDataTripwire.test.ts greps every tracked file for the
identifiers found here and fails on a hit. The exemption list is checked in
both directions, so a stale entry cannot quietly cover a new leak. Two applied
migrations are exempt with the reason stated: editing them breaks their Prisma
checksum on live deployments.
CLAUDE.md rule 59 and docs/domains/seed-data.md rewritten to describe the
allowlist, so the rule and the code say the same thing.
3,457 unit tests pass; tsc and prettier clean.
* fix(tests): align two fixtures the name scrub left half-renamed
Both were case or partial variants the replacement missed, and both failed
only in the integration suite:
- salesPersonFkBackfill asserted a case-insensitive match between a staff
displayName and a differently-cased salesperson string; renaming the
displayName alone made the pair stop matching, which is what the test is for.
- runSalesImport asserted the surname the CSV fixture no longer carried.
Full integration suite passes.
* fix: the seed allowlist refused CI's own database
CI creates a database called `ci` and runs `npm run setup` against it to prove
the first-run path works. The allowlist did not recognise that name, so the
boot-and-smoke job failed at the first step -- a name that declares
disposability as plainly as "scratch" does was being treated as if it might
hold real data.
The gap underneath: the allowlist and the database CI actually creates were two
facts nothing made agree, so the mismatch could only surface remotely, in the
one job that boots the app. seedTargetGuard.test.ts now reads POSTGRES_DB out
of .github/workflows/ci.yml and asserts the guard accepts every value it finds,
and checks setup.sh's shell copy of the rule still carries the same tokens.
Removing "ci" from the allowlist fails both.
3,459 unit tests pass; tsc and prettier clean.
* fix: close four regressions the config seams introduced
An adversarial audit of this branch raised 77 findings; six survived
independent refutation, and I reproduced each by executing the shipped code.
Four were regressions this branch created.
1. ORDORITE_REPORT_PREFIX could not express the two prefixes a real deployment
uses. It was one escaped scalar, so `A|B` became the literal `A\|B` and
matched nothing -- pinning the prefix silently unrouted every report filed
under the other one (stock, PO lines, the 100K-row product catalog), logged
as "skipped" with no alert. Now a comma-separated list, the convention
ORDORITE_STORE_CODES already used.
2. The org-report routes were matching `.+_<name>` UNANCHORED, so files that
previously returned null -- the loud unrouted-file path -- drove master-data
imports instead: Deleted_Customers.csv and Vendor_Stock_by_Item.csv both
routed. `.` matched `/`, so a directory component counted as a prefix.
Now anchored on the base name, with the prefix optional. Unconfigured, the
router matches bare report names only; it refuses a look-alike rather than
guessing. Only Inbound_Items requires a prefix, because a bare
Inbound_Items is a different report with a different runner.
3. Generalising `/^RS\d/i` to `/^R[A-Z]\d/i` looked source-neutral and widened
it 26x. An unrelated R-series -- RA for a rug account, RX for exchanges --
would import as RETURNED and be subtracted from revenue, with no
configuration able to narrow it back. Now ORDORITE_RETURN_PREFIXES, empty by
default: there is no safe universal guess, so a deployment that uses such a
series names it.
4. ORDORITE_STORE_CODES="," was truthy, split to nothing, and joined to "" --
an empty alternation matching a zero-length store code, BROADER than the
default it replaced. isReturnOrder("A1") became true. A misconfiguration
must never widen a match; it now falls back to the default.
Also: setup.sh's globs were unanchored substrings against guard.ts's
word-bounded regex, so setup.sh accepted `holt_samples` and `demolition_prod`,
ran migrate and seed:roles into them, and only THEN did the seed refuse --
leaving a half-set-up database. The shell arm is now word-bounded, and
seedTargetGuard.test.ts compares the two implementations' DECISIONS across 21
boundary names instead of checking they contain the same words. The old test
passed the whole time this was broken.
And a stale COMPANY_EMAIL_DOMAIN restore left the pre-scrub stem in
ordoriteShared.test.ts, where the next test appended to that describe would
have inherited it.
3,466 unit tests pass; import integration suites pass; tsc, prettier, lint clean.
* fix: scrub the real data a second audit pass found, and widen the tripwire
53 findings from the branch audit never got judged -- their verifiers died
mid-run. Re-verifying them confirmed 31, and the leaks among them were worse
than the first pass caught.
People and addresses, all still live at HEAD:
- a customer's name beside her home delivery address, in source, two tests and
a doc comment
- a named customer's spend profile, customer number and return record across
four files
- a former employee named with her order count, and a departed seller's
unattributed revenue
- a customer bound to an outage, an order number and a $91,151 revenue gap
- a customer and two staff first names in a transfer anecdote
- staff names paired with specific orders and commission amounts in a runbook
- the deployment's town ZIP, in two ZIP+4 examples
Half-renames the first pass left behind, each of which is why a scrub needs a
tripwire rather than a careful grep: "Erin Kelly" fixed in one test but left in
another and in the parser it documents; "Karen Dwyer" fixed where the name was
one string and missed where it was split across firstName/lastName; an
"alex robertson" map key orphaned when its sibling was renamed.
Seven vendor fixtures published confidential dealer pricing -- one printed
Wholesale beside MSRP on the same row, another stated the vendor's entire
catalog cost ceiling in a source comment. Every price is now invented, with the
arithmetic each parser actually keys on preserved (qty x price == extension,
extensions summing to the printed total) and the fixed-width caret alignments
in two parser sources unchanged. Real order, PO and reference numbers went with
them, since a document number is what ties an invented price back to a real
deal.
The tripwire now covers all of it: surnames, whole names where the surname is
too common to match alone, addresses, the ZIP, vendor document numbers, the
specific dealer figures, and the catalog-ceiling phrasing. It found five sites
this commit would otherwise have missed, including two where a name wrapped
across comment lines. A rep-group company was deliberately dropped from the
list -- naming a business is not the harm, publishing its trade terms is.
3,474 unit tests pass; six integration suites pass; tsc and prettier clean.
* fix: harden the tripwire, fail the store-code rule closed, repair the renames
Tripwire, which guards everything else and was the weakest part of it:
- It scanned tracked files only, and content only. A file added but not yet
committed, or a file whose CONTENTS are clean but whose NAME is a client's,
both passed. Now scans untracked-but-not-ignored files and every path.
- The catch block adopted any stdout the error carried. A maxBuffer overflow
throws with status null and TRUNCATED stdout, so a partial walk would have
been reported as a clean one -- the exact silent pass the test exists to
prevent. Only a clean exit-1 with no output is a pass now; everything else
throws.
- The stale-exemption check, the suite's only proof it is scanning at all,
exempted the test file itself and so could pass vacuously. That exemption is
gone: the file always hits, because it contains the pattern sources, which
makes it the liveness canary.
- The domain pattern matched only the correct spelling of a name the codebase
itself documents as arriving with typos. Narrowed to the stem.
ORDORITE_STORE_CODES defaulted to `[A-Z]{2,}`, which in front of `[A-Z]*A\d`
matches any letter run whose last letter before a digit is "A" -- SOFA1,
MEGA1234, VIA3 all classified as RETURNED and were subtracted from revenue. No
test could fail on it because every fixture used a real store code. It now
fails closed like ORDORITE_RETURN_PREFIXES; genuine returns are still caught by
the negative-net-total check, so declining to guess costs nothing.
Repairs to the mechanical renames, each of which still compiled and still
passed:
- `akritos` -> `northgate` was collateral. It is the maker's OWN second
deployment, named in NOTICE, SECURITY.md and ARCHITECTURE.md; renaming it in
three files left two names for one thing. Reverted.
- restore-drill.sh's deny-list had been emptied of the non-client names too,
while its comment still promised they were covered.
- apply-preset lost its sentence's subject; journalEntry turned one
deployment's timezone into a false claim about US retailers; four docs were
left with "the the pilot deployment" and lower-cased sentence starts.
- A guard.ts doc comment still named a list this branch deleted.
- The router docs described neither the router nor each other, mixing `SH_` and
`Company_` placeholders. Both are now `<Org>_`, with the env var documented
and the one prefix-required route called out.
Rule 59 and env.example now state that the token must be delimited, so
`holt-demo` and `holt_samples` are refused -- near-misses on purpose.
3,474 unit tests pass; tsc, prettier, lint and markdownlint clean.
* fix(docs): scrub two client mentions #143 brought in on rebase
The tripwire caught these the moment this branch rebased onto the merged #143,
which is the whole reason it exists: the scrub was correct when written, and a
merge from another branch put the name straight back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix: a third sweep found a real customer couple and the dealer prices' twins
A fresh-eyes sweep -- five hunters told nothing about what the earlier passes
had already removed, so they could not be anchored by my assumptions -- raised
60 hits and confirmed 23.
The worst was untouched since the initial public release: "Penny/Steve Sigal",
a real customer couple, in a quarantined it.skip block. Both earlier passes
walked that file and edited lines above and below it. The row's phone and order
number are fictional placeholders, which is exactly why it read as synthetic --
but the file's history shows the author's habit was real names beside sanitized
numbers, so the fake phone vouched for nothing.
The rest is one pattern: the earlier passes scrubbed the FIXTURES and missed
their twins. Every vendor's dealer prices survived in the parser source doc
comment that documents the fixture, in the home-accessory normalizer fixture
that consumes it, or both -- so a comment and the fixture it described no
longer even agreed. Two files neither pass had opened at all, nuorderPrintout
and kkOrderParser, still carried real PO numbers and wholesale prices beside
suggested retail.
All of it now uses the same invented values as the corresponding fixture, with
each parser's arithmetic preserved (qty x price == extension, extensions
summing to the printed total, cancelled-section totals restated) and the
aligned caret comments in two parser sources kept to width.
Also gone: a real ZIP as the worked example in the address parser, and the last
"Erin Kelly" in a JSDoc.
The tripwire now covers the document numbers and the specific dealer figures,
not just names -- it caught the Sigal line the moment the pattern went in. Its
one miss in this pass is worth recording: a bulk substitution applied a short
replacement before a longer one that contained it, leaving a fixture line
arithmetically inconsistent and its test asserting nothing. Tests caught that,
not the tripwire.
3,478 unit tests pass; integration suites pass; tsc, prettier and lint clean.
* fix(tests): the tripwire was the tidiest re-identification kit in the repo
An independent sweep over docs, config, scripts, prisma and file names came
back clean except the two applied migrations already carried as exemptions --
which is the convergence signal worth having. But it surfaced something else
by accident: the guard itself.
The tripwire listed, in plain text and in one public file, the surnames, the
company/town stem, both store towns, the town ZIP, the staff email local-parts
and the phone numbers of a real business. It concentrated in a single place
exactly what every other file had been scrubbed of, and search engines index
it. The guard against publishing a company's identifying data was publishing it.
Patterns are now base64. That is not secrecy -- anyone determined decodes it in
a second, and the header says so. It stops the repo being a plain-text index of
a real company's identifiers, which is the actual harm. `what` stays in plain
English: the REASON an entry exists has to remain readable, only the identifier
is encoded, and the header carries the one-liner for adding a new one.
Removing the plaintext also removed an accident the suite had been leaning on:
the file used to match its own patterns, so it always appeared in the hit set
and doubled as proof the scan was live. That crutch is gone, so the canary is
now deliberate -- a positive control asserting the scan reaches known content
and known paths, and it runs first. Every other assertion here passes on an
empty result, so a scan that silently reaches nothing would otherwise read as a
spotless repo.
Verified both ways: planting a scrubbed identifier still fails two cases, and
the file no longer contains any of the identifiers it guards.
3,479 unit tests pass; tsc and prettier clean.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Found by running the demo, not reading it — the login hung, and the server log explained why.
The loop
logError()records anErrorEvent.recordError()callsreportOpsAlertthe first time it sees a fingerprint.reportOpsAlertlogged itself throughlogError.So every alert produced a new message —
"ops-alert: "prepended to the previous title — which is a new fingerprint, which is a first sighting, which alerts again. Forever, adding one prefix per pass.One unconfigured integration was enough to start it. A missing Axper API key logged once, and the loop turned that into 1,154 ErrorEvent rows and a server too busy to answer a login. Every row was the loop's own output — so the error log, the thing you reach for when something is wrong, was the least usable artifact in the system.
Measured, before and after
GET /appErrorEventafter 3 requestsTwo guards
opsAlert.tslogs throughlogger(stdout, and stops) and says at the top that nothing in it may calllogError. Its two channel-failure paths had the same bug and are fixed the same way.recordErrorrefuses to re-enter the alert path, so no future caller can reopen it from the other side.opsAlertLoop.test.tspins both — and checks the fix wasn't simply "stop alerting", which is the cheapest wrong answer and would pass the first three assertions. Verified in both directions: reintroducing thelogErrorcall fails the test.Also in here — the demo's empty screens
The audit found 59 blocking gaps. These are the ones behind them:
/api/axper/trafficnever readTrafficSnapshot. It made a live third-party call, and that client returns[]for every kind of not-working — no key, bad credentials, vendor outage. The dashboard then rendered "0 ENTRIES TODAY", which doesn't say "we can't see the counter", it says "nobody came in". Now falls back to recorded history. Better product behaviour, not a demo hack: a store whose counter died this morning still sees yesterday.pipeline.ts— 34 open quotes (15 deliberately stale, so Stale Quote Cleanup has something to find), 22 leads (8 unassigned, because that's what the Assign action is for), 60 interactions (3 still in progress), 6 B2B proposals across DRAFT/SENT/ACCEPTED. Four screens were empty behind this one absence.setupData.ts— gift-card presets (their absence dead-ended the whole gift-card sale flow), trade tiers, email templates, label templates, sales goals, the up-board, and two years of door-counter traffic. Two years because the dashboard compares against the same day last year, and one year makes that read zero. Inserted withcreateMany— 11,700 rows one at a time turns a two-second step into a two-minute one, and a seed nobody wants to run is a seed that goes stale.2026-08-01, so a demo went stale a day at a time — three weeks on, the first screen a viewer sees reads$0.--as-ofstill pins it for reproducibility; nothing in the test suite depended on the constant (checked, not assumed).Verification
3,444 unit tests pass.
inventoryAllocation,errorRecorderandtradingDayintegration suites pass.tsc— 0 errors.🤖 Generated with Claude Code