top accounts: read the tags the list actually carries

The addresses list endpoint returns a projection of an address — no tags, no
name, no implementations. AddressesListItem guarded with `public_tags !== null`,
which is true of an absent field, so every visit to /accounts read `.length` off
undefined and the whole page fell to the error boundary. The type said the field
was always present, so nothing caught it.

Partial<AddressParam> states what the endpoint sends, and both read sites reach
for the tags the same optional way.

The error screen it fell to had its own defect: the status glyph is an
inline-block div and gui's H1 renders inline, so the two shared a line box and
"500" ran into "Oops! Something went wrong". The screen is a stack; it now says
so. The heading's own top margin was being outranked by gui's atomic reset.
This commit is contained in:
2026-08-28 23:45:54 -07:00
parent cea7badf29
commit 5bbb57995a
5 changed files with 23 additions and 7 deletions
+10 -2
View File
@@ -1,6 +1,14 @@
import type { AddressParam } from './addressParams';
export type AddressesItem = AddressParam & { transactions_count: string; coin_balance: string | null };
// The list endpoints carry a projection of an address, not the full record:
// tags, names and implementations are resolved per-address and are simply
// absent here. Partial says so, which is what forces the read sites to reach
// for them optionally instead of assuming a null.
export type AddressesItem = Partial<AddressParam> & {
hash: string;
transactions_count: string;
coin_balance: string | null;
};
export type AddressesResponse = {
items: Array<AddressesItem>;
@@ -9,7 +17,7 @@ export type AddressesResponse = {
hash: string;
items_count: number;
} | null;
total_supply: string;
total_supply?: string;
};
export interface AddressesMetadataSearchResult {
+1 -1
View File
@@ -44,7 +44,7 @@ const AddressesListItem = ({
<span>{ index }</span>
</Skeleton>
</div>
{ item.public_tags !== null && item.public_tags.length > 0 && item.public_tags.map(tag => (
{ item.public_tags?.map(tag => (
<Tag key={ tag.label } loading={ isLoading } truncated>{ tag.display_name }</Tag>
)) }
<div className="flex flex-row gap-3 max-w-full items-[flex-start]">
+2 -2
View File
@@ -46,9 +46,9 @@ const AddressesTableItem = ({
className="font-bold my-[2px]"
/>
{ item.public_tags && item.public_tags.length ? item.public_tags.map(tag => (
{ item.public_tags?.map(tag => (
<Tag key={ tag.label } loading={ isLoading } truncated>{ tag.display_name }</Tag>
)) : null }
)) }
</div>
</TableCell>
<TableCell isNumeric>
+6 -1
View File
@@ -110,7 +110,12 @@ const AppError = ({ error, className }: Props) => {
})();
return (
<div className={ `mt-[52px] lg:mt-[104px] max-w-[800px] ${ className || '' }` }>
// The pieces below — status glyph, heading, copy, button — are written as a
// plain sequence and read as a stack. Nothing here can rely on them being
// block-level: the glyph is an inline-block div and gui's H1 renders inline,
// so in normal flow they share a line box and the glyph runs straight into
// the headline. A column lays them out as the markup reads.
<div className={ `mt-[52px] lg:mt-[104px] max-w-[800px] flex flex-col items-start ${ className || '' }` }>
{ content }
</div>
);
+4 -1
View File
@@ -6,8 +6,11 @@ interface Props {
title: string;
}
// gui's H1 emits its own atomic margin reset (`_marginTop-0px`), so a plain
// `mt-8` here rendered as zero and the heading sat flush against the status
// glyph above it. The modifier is what outranks the reset.
const AppErrorTitle = ({ title }: Props) => {
return <Heading className="mt-8" level="1">{ title }</Heading>;
return <Heading className="!mt-8" level="1">{ title }</Heading>;
};
export default AppErrorTitle;