refactor(miner-ui): adopt shared StateBoundary in the portfolio route - #6588
Conversation
…JSONbored#6511) Replace the hand-rolled loading/error/empty <p> tags in PortfolioQueueView and PortfolioQueueActionsSection with the ui-kit StateBoundary, plus content-shaped Skeleton placeholders so the layout does not jump when the 10s poll lands. Each user-visible sentence is passed as the whole EmptyState/ErrorState title with the description suppressed, so the rendered copy is unchanged from the <p> tags it replaces. Each fetch keeps its own boundary: the summary and queue-actions reads are independent, so one failing must not blank the other. Closes JSONbored#6511
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-16 16:13:43 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|


Summary
Both components in this route hand-rolled the same loading/error/empty triple as literal
<p>tags, and the loading branch was a flat sentence re-rendered on every 10s poll. This swaps both onto the ui-kit'sStateBoundaryplus itsSkeletonprimitive — which this app already depends on but had never imported.Prerequisite confirmed before starting, as the issue requires: the
state-views.tsxport has landed —@loopover/ui-kit/components/state-viewsexportsStateBoundary/LoadingState/ErrorState/EmptyState. I import the real component from the package; nothing is forked, copied, or reached into fromapps/loopover-ui.Design decisions
Each async flow keeps its OWN boundary, deliberately. The summary read and the queue-actions read are independent fetches; one shared boundary would let a queue-actions failure blank the summary, or a summary failure hide the actions.
Skeletons are shaped like the real content — three status cards over table rows, and rows for the actions table — so the layout settles when the poll lands instead of jumping. A single generic bar would just move the jump later.
Every user-visible sentence is preserved exactly, character for character. This is the one part worth scrutinising, because the obvious way to adopt this primitive gets it wrong.
StateBoundarywants atitle+ adescription, which invites splitting each existing sentence across the two — a rewrite, however reasonable, of copy the issue says not to reword. Instead each whole original sentence is passed as the title and the description is suppressed, so the rendered text is identical to the<p>it replaces. The suppression is not symmetric, and the difference matters:EmptyStatehas no default description andShellrenders{description && …}, soemptyDescription={null}renders nothing.ErrorStateresolvesdescription ?? <default>, sonullwould restore its default copy — it needserrorDescription=""to stay suppressed.The result: none of
StateBoundary's own boilerplate ("This view has no records to show.", "Something went wrong fetching this data.") reaches the user.ErrorStateemitsrole="alert"itself, so failures keep announcing exactly as the hand-rolled<p role="alert">did.Frozen underneath — the part worth checking
Per the issue, the action surface is behaviourally untouched, and the diff proves it:
lib/portfolio-queue.ts,lib/portfolio-queue-actions.ts,lib/use-polled-fetch.ts— unchanged (git diffagainstmainforsrc/lib/is empty).Buttons and theironClick/disabled={pending}wiring toonRelease/onRequeue→releaseItem/requeueItem— unchanged. Only the chrome around them moved.The strongest evidence is the tests: every pre-existing assertion in both suites passes unmodified, including the release/requeue POST-wiring tests, the pending-disable test, and the #6090 regression (a failing release renders the error without a false re-fetch). Those untouched tests are what prove the action surface really didn't move.
Tests
The two loading assertions had to change, because
StateBoundaryrendersloadingSkeleton ?? <LoadingState>— with a skeleton supplied, the literal "Loading local portfolio queue…" / "Loading actionable queue items…" text is intentionally gone. Both now assert the skeleton placeholder instead, exactly as the issue directs.Beyond that, each of the four boundary branches is now pinned to its exact rendered sentence, since preserving that copy is the main risk in this change and a loose regex would not have caught a reworded split:
portfolio-queue.test.tsxThis view has no records to showSomething went wrong fetching this dataportfolio-queue-actions.test.tsxValidation
npm --workspace @loopover/ui-miner run test(vitest run --coverage), no coverage-threshold failure. That's the operative local gate.eslint— 0 errors on my three files. The 3 remaining warnings (react-refresh/only-export-components, tworeact-hooks/exhaustive-deps) are pre-existing onmain, not introduced here.prettier --writeapplied.git diff --checkclean; the tree contains only the three permitted files (no generatedrouteTree.gen.tschurn). Rebased on latestmain— no base conflict.One pre-existing failure, not mine:
ui:typecheckreportsvite-chat-api.ts(43,34) TS2352. I verified it fails identically on cleanmainwith my work stashed.Coverage
Not scored by
codecov/patch:apps/loopover-miner-uisits underapps/**, whichcodecov.yml'signore:list excludes (Codecov collects onlysrc/**,packages/loopover-engine/src/**,packages/loopover-miner/lib/**). Flagging that explicitly so a reviewer isn't confused by the absent check — the app's own local coverage floor above is the real gate, and it's green.Scope
routes/portfolio.tsx,portfolio-queue.test.tsx,portfolio-queue-actions.test.tsx.run-history.tsx,ledgers.tsx,index.tsx,__root.tsxuntouched — they're separate issues.@loopover/ui-kitprimitives.site/,CNAME, orlovablechanges.Safety
Closes #6511