View Markdown Β· Documentation index (llms.txt)
Angular bundle hand-patch β RETIRED 2026-09-30
v20 (2026-10-01): durable settlement ledger β settled state survives refresh, a second browser, and an adapter restartβ
Operator report on angular: opening another browser reset rows to SETTLE
buttons for trades already settled, and some rows stayed on SETTLING⦠after
BankerX settled them. Root cause verified in code: the settlement ledger
lived ONLY in localStorage(traderx_settlement_ledger_v1, per-browser)
and the adapter served every trade with its in-memory lifecycle state while
holding no settlement status at all β so the ONLY place settlement truth
existed was a single browser's localStorage. The desk-reuse v19 work landed
first; v20 is this fix.
Fix (three pieces, minimal delta):
- Adapter durable registry (
synaptic-traderx-adapter/server.mjs, :8415) β file-backed JSON (settlements.jsonnext to server.mjs) keyed by UETR.GET /trade-service/trade/trades/settlements/returns the map;POSTthe same path publishes{entries:[{uetr,status,tradeId, security,quantity,side,created,txSignature?}]}. One-way ISO 20022 ordering (Pndg β Acsc/Rjct, never backwards) is enforced SERVER-side. Routes sit before the/trade//tradessubstring checks (the trade-service prefix itself contains/trade). Verified: PndgβRjct merge accepted, later Pndg merge ignored; registry survivespm2 restart. - Blotter publish + reconcile β
persistSettlementLedger()now also publishes the ledger (entries carry the trade fingerprint: security, quantity, side, created) fire-and-forget; a newreconcileFromRegistry()(constructor, every 3s alongside the snapshot poll, and once whenevermergeSnapshotlands new rows) adopts entries under two guarded paths: (a) a UETR the browser already tracks β its own stuck rows cure themselves; (b) a fingerprint-matched tradeId for rows dispatched in a DIFFERENT browser. A registry entry can never stamp SETTLED onto an unrelated row: incomplete fingerprints block fingerprint adoption, and the same tradeId regenerated after an adapter restart (which re-seeds in-memory TRADES with a newcreated) fails the fingerprint and is correctly NOT adopted (demonstrated live: B did not adopt a pre-restart entry after the adapter restarted). - Solana memo watcher port β the root blotter's
checkOnChainSettlementlogic now runs in the angular blotter too (2.5s beat,getSignaturesForAddresson the creditor token account, memo contains the row's UETR β authoritative Acsc, because the desk settles only after its WOTS+ re-derivation passes and a failed attestation creates no settlement). Resolved rows publish to the registry with their tx signature so every other browser reconciles within a tick. This is what cures rows that stayed SETTLING because the page that missed the desk's live status broadcast was the only witness.
Deployed bytes: bundle main-FDMPPVWA.js (ca10adb4β¦) + angular index
5143aad4β¦, bundle ref ?v=20; agent stays ?v=19 (dcfc8cb4β¦, desk
reuse), bridge ?v=13, root pages unchanged. Served==disk verified per
URL-bust. Preflight 25/0 DEMO-READY (FROZEN_C/D re-frozen to v20).
Verification with real settles (operator directive: verify live):
- Probe 1 (drain, USD/KES seed row): browser A settled (
tx 62akSM7Rβ¦); a FRESH second browser context β zero shared storage β showed the row SETTLED on fresh load and STILL SETTLED after reload. - Probe 2 (drain, same row again): A settled again (
tx hkie33K9β¦), registry held both entries across an adapterpm2 restart. - The adapter registry was then cleared (
{}) at the operator's clean-slate directive; probe artifacts are recorded here, not in the live registry.
Operator Q&A (restart / clean slate): the traderX root and angular pages
are nginx-served STATIC files β there is no process to restart, and none
was needed; the only pm2 component in the flow is synaptic-traderx-adapter
(:8415), which was restarted with the new server. To reset the slate:
empty settlements.json + pm2 restart synaptic-traderx-adapter (trade
seeds re-materialize with fresh identities; old ledger rows can never
collide because of the created fingerprint).
v19 (2026-10-01): UETR in the STATE column after settlementβ
Operator correction after v18: the UETR was never supposed to land solely in the ACTION badge β the STATE column kept showing the adapter trade-lifecycle text ("Pending") on every row, including long-settled ones, which reads as wrong on the desk. v19 changes exactly one thing plus one repaint fix:
- STATE column shows the UETR once the settlement is known β a new
settlementStateCellHtmlSTATE-column renderer emitsuetrLine(uetr)(truncatedfirst8β¦last4, full value on hover) for rows present in the settlement ledger, and keeps the raw trade state for rows with no dispatch. The ACTION column is UNCHANGED from v18 (SETTLE button β SETTLINGβ¦/SETTLED/REJECTED badge + UETR); a v19 interim build that moved the badge into STATE never shipped verification and was replaced the same hour. refreshCells({ force: true })β a plainrefreshCells()only re-renders cells whose value changed; the STATE field (e.g. "Pending") never changes, so the new content only painted after a reload. force makes the STATE UETR paint live on the dispatch round trip.
Deployed bytes: main-7E7PBSO6.js (add329e9β¦) + angular index
29b46e1cβ¦, bundle ref ?v=19; agent ?v=15, bridge ?v=13, root index
unchanged. Verified served==disk per URL-bust. Suites on these bytes:
angular conf Γ2 (131/1 both β sole failure the disclosed Resolve1 churn),
probe Γ1 all gates green incl. p6 uetr-shown-in-state and p0 pre-settle
plain state. Preflight 25/0 DEMO-READY.
v18 (2026-09-30 evening): UETR in the state column + pacs.002 ordering guardβ
Clean source rebuild (same recipe as v15/v16/v17). Two additions to
trade-blotter.component.ts, both driven by the operator's Oct-1 flow
requests:
- UETR under the badge β
settlementCellHtmlappendsuetrLine(uetr)to every non-button state: the truncated UETR (first8β¦last4, 9px monospace, muted) with the FULL UETR on thetitleattribute. The UETR is the pacs.008 β pacs.002 correlation ID; reviewers can now trace a row into the desk receipt modal / explorer without opening anything. Lifecycle stays one-way:SETTLINGβ¦βSETTLED(~15s trilateral round trip live-traced; see receipt). - ISO 20022 status ordering guard β
applySettlementStatusnow refuses to move a row backwards: pacs.002 terminal statuses (Acsc/Rjct) are one-way; a late or re-relayed non-terminal report on the same UETR is logged and dropped (ignored non-forward status report). Today no estate path emitsPndgpost-dispatch; the guard makes the invariant structural ahead of any future mid-flight status. No fabricated status is ever sent to exercise the negative path (no-fabrication principle).
Deployed bytes: main-I5PEKJGD.js (b694f8c7β¦) + angular index.html
(3c8e4f66β¦, bundle ref ?v=18); agent ?v=15, bridge ?v=13, root
index unchanged. Verified served==disk per URL-bust; preflight re-pointed to
v18 β 25/0 DEMO-READY. Suites on the new bytes: angular conf Γ2 (131/1 then
132/0), angular E2E Γ2 all-gates green (G8 SETTLED cell Trade-6),
blotter probe Γ1 with the added p6 uetrShown gate (all six gates green) β
ops/FDC3-CONFORMANCE-RECEIPT-2026-09-28.md Β§"v18".
v17 blotter-fix (2026-09-30, after the v16 batch)β
NOT a hand-patch β a clean source rebuild. Two user-facing blotter defects, both root-caused in source:
- Ticket-create not visible without window refresh. Root cause: the
estate Synaptic adapter (
synaptic-traderx-adapter, :8415) implements the trade-feed Socket.IO path as a handshake mock β a fake sid with no engine connection, so nocreateTradeTicketnotification ever reaches the blotter's feed subscription. Fix (source,trade-blotter.component.ts): poll the real REST snapshot (GET /trades) every 3s and merge in only NEW trades (mergeSnapshot) β existing rows keep object identity, ag-GridgetRowIdcontract untouched. - Settled rows reset to SETTLE after reload. Root cause: the settlement
map (
settlementByRow) was component-scoped in-memory state; nothing restored it and the status correlator can only match UETRs already in the map. Fix (same file): persist the ledger β real dispatched facts only β tolocalStorage["traderx_settlement_ledger_v1"], restore at construction, persist again on every status transition (Pndg β Acsc/Rjct) and on the honest dispatch-failure rollback.
Deployed bytes: main-V6THKHFV.js (707039faβ¦) + index.html (c8191c56β¦),
agent ?v=15 / bridge ?v=13 untouched; preflight updated to assert v17
(ops/oct1-fdc3-demo-preflight.sh). The 016 frontend-overrides copies of
trade-blotter.component.ts + fdc3-interop.service.ts were re-synced to the
applied source (they had drifted behind the v15/v16 fixes).
v16 G8-fix (2026-09-30, later the same day; commit e10ad26)β
The Γ2 batch on v15 bytes failed exactly one gate, G8 (the SETTLED badge),
twice β while the run log showed the status fully landing
([fdc3] settlement status received {uetr β¦, status: Acsc}, i.e.
receiveSettlementStatus passed its type + dispatched-UETR gates and
settlementStatus$.next fired). Root cause is not in the status chain at all:
refreshSettlementCells() reset rowData with the same trade object
references ([...this.trades] copies the array, not its items), and with
getRowId set ag-Grid treats row data as immutable β a rowData reset alone
does not re-run cell renderers, so the already-updated settlementByRow map
never got re-painted. The estate (root-index) surface is unaffected because it
full-renders the blotter after the same mutation. Fixed in source
(fdc3β¦ trade-blotter.component.ts): gridApi.refreshCells() after the
rowData reset. Proved Γ2 on the deployed v16 bytes
(main-L3LD77TD.js 26361d9bβ¦, index c557f372β¦).
v15 flow-fix (2026-09-30, later the same day; commit d9179ee)β
The source-rebuilt bundle exposed a dead SETTLE gate: the state as served
showed no SETTLE column because (a) nothing on the blotter view initialized
the FDC3 interop service, and (b) the StartPayment availability probe read
resolution.apps/resolution.appIntents off a response the estate DA
delivers in the FDC3 wire shape {appIntent:{intent,apps}}. Both fixed in
source (fdc3-interop.service.ts, trade-blotter.component.ts), plus the
separate-window desk launch (estate-agent.ts open() popup features) and the
mobile-fluid blotter containers (trade.component.scss, terminal already
page-fluid at 390/620 β no terminal bytes changed).
Honest caveat: the archived hand-patched bundle below is not a byte-exact
record of what was served this morning β the live bundle that passed the
14:24β14:25 E2E runs evidently carried additional estate deltas in the
availability region (it rendered the SETTLE column; the archive's parse
could not). Fingerprint parity on markers stays true; behavioral code parity
of the pre-v15 bundle does not. The v15 source fixes supersede and close the
question β the current bytes (main-WFYOIDOI.js) ship the fixed logic in
real TypeScript.
The source-rebuilt bundle (retired again 2026-09-30 by the v15 flow-fix)β
The hand-patch described below is no longer served. The pair-mapping logic now lives in real TypeScript source and the deployed bundle is a clean rebuild from it:
- Source:
templates/web-front-end/angular/main/app/trade/trade-ticket/trade-ticket.component.tsβonCreate()normalizes the selected company label toUSD/KES/EUR/USD(viaincludes) and otherwise passes the selected company throughFdc3TickerCompatibilityBridgeService.normalizeTicker(single canonical normalization location). - Deployed:
main-ZMJ5JEK4.js(md5eca76fccac04c67f69aff428179aa73a), builtng build --configuration production --base-href /angular/from the tree with the 016 frontend overrides applied (per../generation-hook.md). - Fidelity proof: rebuilt bundle fingerprint-matches the previously served
bundle on every estate marker (
settlementByRow8,StartPayment11,getAgent8,UETR6,Pndg6, desk strings,runtimeMetadataUrl2β¦); the only intended deltas are the onCreate normalization port and the resolveAgent fast-path made explicit. Byte diffs elsewhere are minified identifier renames. - Source-side extras needed to reproduce the deployed build tree (all
committed here): 016
fdc3-interop.service.ts(incl. the estate resolveAgent fast-path),Account.name?,Trade.price?,@robmoffat/fdc3-get-agentdependency,skipLibCheck. - Superseded bundle archived:
evidence/main-SZ7POL5E.js.hand-patched.bak(md573fbb1c9571202a3fba214acea1ea02b) β the previously served hand-patched copy (retired from the web root same day). - Pristine upstream evidence unchanged:
evidence/main-SZ7POL5E.js.bak(pristine, 1,663,875 bytes) andevidence/index.html.bak.
The retired patch (historical record)β
main-SZ7POL5E.js (served at /angular/main-SZ7POL5E.js until 2026-09-30
carried one hand-written injection vs the pristine upstream build
(evidence/main-SZ7POL5E.js.bak β pristine copy, moved out of the web root
2026-09-30 so the served directory carries no trail):
- Location: TradeTicket component
onCreate(byte ~435010). - Size: +633 bytes.
- Behavior: after the Create-Ticket form submits, the selected company
string is normalized to a currency pair:
contains("KES") || contains("USD/KES")βUSD/KES,contains("EUR") || contains("EUR/USD")βEUR/USD, otherwise the selected company passes through unchanged. Without it the grid rows would carry raw company names and the post-trade settle flow (which buildsfdc3.paymentfrom the row'ssecuritypair) would have no pair to settle against.
2026-10-12 β templates settlement code moved to the 016 overlayβ
The fdc3-interop.service.ts, trade-blotter.component.ts,
position.service.ts, account.model.ts and trade.model.ts copies that
used to sit in the shared templates/web-front-end/angular tree are gone from
the shared tree: 014's baseline and 016's pack overlay are the only sources
(now specs/016-post-trade-settlement-bankerx/generation/frontend-overrides/ ...carries all of them, plus the newer cash-leg mapping). The canonical app
bundle build source is unchanged β but a rebuild MUST apply the 016
frontend-overrides on top of the generated trees (per
../generation-hook.md), which is now also scripted in
scripts/start-state-016-post-trade-settlement-bankerx-generated.sh. The
TradeTicket company-name normalization block stays in the templates tree
(estate build source; the generated trees take 014's trade-ticket override,
so the block never reaches a generated baseline).