The detail page rendered a flat grid of every field the view returned — 29 for
Invoice Detail, 21 for Finance Review Detail — so a reviewer's actual decision
inputs sat among twenty peers of equal weight, and the most valuable data in the
app wasn't shown at all. InvoiceDetail now orders the page by the decision being
made: identity+status, AI recommendation, 3-way match, clarification exchange,
source document, AI reasoning trail, history, then the original grid collapsed.
AIActivityPanel ports the studio builder's AI Employee -> Logs tab into the app,
narrowed to one instance: decisions with status/confidence/model/reasoning/
reflexion, escalations, and a per-run trace waterfall with durations, tokens and
cost. Same event vocabulary and confidence thresholds as the builder, so a client
never has to be shown the builder to see what the AI did. The endpoints
(/monitor/instances/{id}/log and /trace) are publicly routed and accept the app's
own JWT.
AuditTimeline finally uses getAuditLog, which had been written and never called
from anywhere. It renders the trail as a story — who acted, AI or human, what
state resulted, and how long each hop waited — deduping the extra rows the
trigger's performActivity commit produces (9 raw entries collapse to 4 on
instance 128).
ThreeWayMatchCard surfaces match_summary and analysis_so_far. Both were
effectively invisible: one buried in the field grid, the other explicitly dropped
by DetailViewPanel's SKIP_KEYS for looking like raw JSON. analysis_so_far is
actually the AI's gathered evidence — PO, goods receipt, contract, payment
history and the specific discrepancy — so the recommendation becomes auditable
instead of a wall of prose.
InvoiceDocumentCard makes the uploaded PDF viewable. invoice_pdf appeared in zero
record views and zero detail views, so the department answering a clarification
and the reviewer approving payment could not open the document they were
reasoning about. The /download endpoint forces an attachment, so the bytes are
fetched as a Blob and shown inline via an object URL.
Display metadata keys off state and activity UUIDs rather than the names the API
returns, so nothing depends on matching "Financial Review" as a string.
Verified against live app 169: instance 128 (3 decisions, 3 traces correctly
linked, full match evidence, clarification Q&A) and 161 (OCR submission, PDF
resolves and downloads, AI panels show empty states rather than errors).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "Create New Invoice" button was configured all along, on the rv_screen
layout rather than the screen layout, and was being dropped twice over:
RecordViewTable fetched the rv_screen but kept only rv_template_uid and
threw away `layout`, and ScreenRenderer matched only the legacy
params.activity_id_init while current studio writes activity_uid_init.
collectStartButtons deep-walks either shape — the button nested under a
node's `config` as screen layouts do, or carrying type/params/job_template
at its own top level as rv_screens do — and accepts activity_uid_init,
activity_id_init, activity_uid and activity_id, newest key first.
RecordViewTable renders any start_state_machine buttons from its rv_screen
in the toolbar alongside the injected toolbarAction; ScreenRenderer routes
through the same helper and now skips buttons with no resolvable activity
instead of rendering a dead control.
Verified against app 169's live rv_screen config: yields exactly one button,
"Create New Invoice" -> Submit Invoice, ignoring the show_detailview_popup
entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>