Conversation
Dashboard: - min/max value filters on Delegates (voting power) and Token Holders (balance) - Delegates as default tab; sidebar renamed to "Stakeholders" - larger rows with bottom borders, continuous activity ring, VP as percent of quorum - inactive-delegate flag and 0/0 states (Inactive / No proposals / Never voted) - inactive-VP alert banner on Token Holders; "Voted X/Y (Inactive)" on delegate column - clickable addresses that re-point the drawer; per-address Activity tab - Balance History In / Out / Vesting; dust badge and Hide dust switch on Top Interactions - VP History low-importance filter and All time; time selector MAX plus custom calendar range - delegate drawer tabs renamed (Voting Power, Delegation History); Former Delegators view - proposal final-result filter on the votes tab API: - new endpoints: voting-powers/inactive-summary, accounts/:address/delegators/historical, addresses/labels; address filter on feed/events; proposalStatusIn on proposals-activity Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address enrichment (ENS, contract flag, arkham labels) had no staleTime, and the global QueryClient defaults to 0, so every EnsAvatar/TypeCell refetched on each remount. Table re-renders (amplified by the inactive banner and per-row activity fetch) remounted rows and fired/canceled these requests repeatedly, flooding the address-enrichment API on every tab open. Give the useGetAddress and useGetAddresses calls a 5m staleTime / 30m gcTime so identical addresses dedupe and stay cached across remounts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- votes: rename user-vote filter labels For/Against to Yes/No - voting power: DS SegmentedControl for current/former view - voting power: summary (Current VP / Total VP Lost) on the selector row - former delegators: short date (Jan 3, 25), dedup VP impact when unchanged - delegation history: net VP change on graph, low-importance toggle on CSV row - activity: DS SegmentedControl for date and relevance - token holders: banner uses DS InlineAlert, table fills height below it - top interactions: hide-dust on CSV row, total as USD, Net Tokens In/Out (90D) - balance history: net balance change value on graph - period label: All time renamed to Max available data - table: footerActions slot; inline alert accepts ReactNode content Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- EnsAvatar: optional subtitle slot rendered under the name; the avatar stays vertically centered against the whole name + subtitle block - token holders: delegate column renders "Voted X/Y" via the subtitle slot so the avatar aligns with both lines - table: row dividers now live on the cells, since border-separate tables do not paint borders set on the <tr> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified against the Figma frame measured pixel by pixel, and against the locally rendered table: - delegate cell: avatar is vertically centered against the whole name + "Voted x/y" block (measured offset 0), and the subtitle starts at the same x as the name, as in the design - row borders: border-separate tables never paint borders declared on the <tr>, and the first cell additionally cleared them on desktop, so the divider was missing entirely and never reached the Address column. The line is now drawn by a cell pseudo element spanning the full cell width, ignoring the horizontal padding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- address column: long arkham labels are truncated instead of overflowing. The chain was broken by the tooltip trigger button, an inline-block whose shrink-to-fit width was set by the label, so `truncate` never had a width to work with. Clamping the trigger fixes every table using EnsAvatar. - avg vote timing: shows a skeleton while the per-row proposals activity loads, instead of the "-" it uses for delegates with no votes. - calendar popover: uses rounded-base, so the radius follows the DS token (0 for Anticapture, non-zero for whitelabels) instead of a hardcoded md. - token holders change column: right aligned, matching the delegates tab. - balance history and voting power graphs: the heading no longer changes between loading and loaded states. - drawer activity: the feed is scoped to the inspected wallet again. The address filter was fine; "All" omitted `relevance`, which the API reads as MEDIUM, so its value thresholds hid almost everything. The API now takes relevance=ALL to drop the threshold, and the drawer always sends the value explicitly. - drawer activity: infinite scroll works. The observer used the viewport as root while the list scrolls in its own container, leaving the sentinel on the clipped edge and the feed stuck on page one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ment - holders and delegates page title reads "Stakeholders", matching its nav entry - delegate votes: the rate metric card is labelled "For Rate" - former delegators: VP Impact header aligns left - voting power summary: the loading skeleton aligns left with the value it replaces Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An ENS record can be a 42 character address plus ".eth", which overflowed the address column and ran into the next one. DrawerAddressButton wraps the avatar in a button, and a button is inline-block, so it was sized by its content and the name below it never had a width to truncate against. Clamping it fixes every drawer table that renders an address. VP Impact is centered in both the header and the cells. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rated enum The previous guard compared against "ALL" directly, which only typechecks when the client happens to be generated from a spec that already exposes that value. CI regenerates the client against whichever Gateful it can reach, so the comparison broke there. Checking membership in the tiers this page offers works either way. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dule
Retry loop: useDelegatesActivity only recorded addresses whose fetch succeeded,
so a rejected (or empty) response left the address selectable again as soon as
it left the loading set. Since the effect depends on both sets, that refetched
the same addresses forever while the endpoint kept failing. Fetches now settle
into their own set, and one address failing no longer discards the others.
AAVE: its API registers no proposal endpoints, so the shared TokenHolders was
firing a 404 per delegate for proposals-activity and a 400 for the inactive
summary, which falls through to the /voting-powers/{address} param route. Both
are now gated on the DAO exposing proposal activity.
activityFromDate was recomputed from Date.now() on every render, so it changed
whenever a render crossed a second boundary and re-keyed both the per-delegate
activity cache and the banner query. Memoized on its inputs.
Total VP Lost summed only the pages already loaded while printing the API's
count of every former delegator beside it. It now claims a total only once
there is nothing left to load.
Canceled proposals are no longer votable in the inactive summary window, so it
agrees with proposals-activity instead of reporting a delegate as inactive for
skipping a vote that never happened.
Also: failed requests in the drawer activity feed and the former delegators
table no longer render as "nothing found"; the USD column shows a dash instead
of a confident "$0" while the token price is in flight; the Total Interactions
tooltip describes the value it actually shows; and an address inside
DrawerAddressButton no longer nests the tooltip trigger button inside the row
button, which cost a second tab stop and hijacked the accessible name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter matched the stored proposals_onchain.status, but the service overwrites each proposal's status at read time with a derived value, and the indexer only ever persists ACTIVE, CANCELED, EXECUTED, PENDING, QUEUED and VETOED. DEFEATED, SUCCEEDED, NO_QUORUM, EXPIRED and PENDING_EXECUTION could therefore never match: on ENS "Failed" returned zero rows and zeroed all four metric cards, "Passed" silently dropped SUCCEEDED, and "Canceled" was unsatisfiable because the query already excludes canceled proposals. Making it correct means persisting the derived status from the indexer, or expressing the derivation in SQL over end block, vote tallies, quorum and timelock. That is its own task, so the filter and the proposalStatusIn param come out for now. The unrelated user vote filter on the same table stays. Also drops the "(90D)" from the Net Tokens In/Out label, which claimed a window the request never asked for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gs link on its table The activity requests only ever sent fromDate, so with a custom range that ended in the past "Voted X/Y" counted every proposal from the range start up to today. The banner above the same table did send toDate, so the two disagreed about the window they described. Both hooks now send both bounds. Making Delegates the default tab broke the DAO overview entry point: the "Biggest holdings change" card links to holders-and-delegates with no tab param, so it landed on Delegates instead of the holders table it describes. It now asks for tokenHolders explicitly, like its delegate-side sibling already did. Also hardens the activity fetch in useDelegates the same way useDelegatesActivity was hardened. The retry loop there predates this PR, but promoting Delegates to the default tab makes it the first thing most visitors hit, so a failing proposals-activity endpoint would now hammer the API from the landing tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex: - Namespace the page-level custom range as rangeFrom/rangeTo. Plain from/to belong to the drawer's Balance History address filters, so a custom range was sending date strings as addresses and getting cleared on tab changes. - Collapse delegation rows per source event before sequencing former delegators. DAOs with partial delegation (SCR) write one row per delegatee out of a single DelegateChanged, sharing tx hash, log index and timestamp, so sibling delegates were read as moving away from each other. Only name a redelegation destination when the move-away event points a single delegation away from the queried address. - Move FeedEventItem into shared/. It also imported EntityType back from holders-and-delegates, so that type moves to shared/types/entities.ts, which clears two pre-existing violations in dao-overview too. - Drop the proposal final-result filter clause from the changeset; it was removed from this PR in 30daf70. isadorable: - Restore the "Holders & Delegates" page heading. Only the sidebar entry becomes "Stakeholders", per the DEV-562 decision. The subtitle prop that was meant to preserve it is dead: TheSectionLayout never rendered it, so drop it from the call sites and from the props type. - Replace the activity ring's hardcoded hex with stroke-border-contrast and stroke-success. The track was dark-mode --base-border, which rendered as dark grey on a light background. - Give the AAVE delegates table the DEV-476 min/max filter, and document why it is a reduced version of the shared Delegates table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
API: - proposals-activity accepts an optional `toDate`, enforced on the proposal timestamp in both the page and the analytics queries. The dashboard already sent it for custom ranges, where it was silently discarded, so bounded periods counted every proposal through today. - the feed's delegation enrichment keeps the row that mentions the filtered address. Partial delegations (SCR) write one row per delegatee out of a single DelegateChanged, all sharing tx hash and log index, so collapsing them by key could describe a delegate unrelated to the filtered address. Dashboard: - balance-change and voting-power-change totals read their period boundaries from their own limit-1 lookups instead of the plotted rows, which are capped at 1,000 and hide small events. Active accounts were reporting the change over a truncated suffix of the period. - "Hide dust" moves into the interactions query. Filtering client-side could empty a page, and an empty table drops the infinite-scroll sentinel, leaving qualifying rows on later pages unreachable. - per-address activity fetches carry a generation for the DAO and range, so a superseded response can no longer merge stale proposal counts into the rows. - the drawer's Activity tab only renders for DAOs whose API serves the feed; AAVE showed a permanent error state. An unknown tab in the URL now falls back to the first one instead of an empty body. - clicking an address in the drawer feed carries its entity type, so a delegate opens the delegate profile rather than the token-holder tabs. - the custom range calendar can apply a single day, via an explicit Apply. react-day-picker answers the first click with `from` equal to `to`, so the old inequality check made one specific day unselectable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…feat/holders-delegates-v3
…0860) Every in-drawer address column now states which kind of profile it points at, so DrawerAddressButton sets the drawerEntity override the same way the drawer's activity feed does. Clicking a delegator in a delegate drawer no longer opens that holder with delegate tabs, and clicking a delegate from a token holder's delegation history no longer keeps token holder tabs.
The holders and delegates tab param is parsed as an enum instead of a plain string, so a stale or hand-edited ?tab=foo coerces back to the default tab rather than missing every key in tabComponentMap and rendering an empty section body.
…eview #3675140866)
Partial delegation writes one delegations row per delegatee out of a single
DelegateChanged, so the primary row alone describes the event badly: an
unfiltered feed renders one arbitrary delegatee, and a feed filtered by the
delegator matches every sibling row, so picking one drops the others.
FeedDelegationMetadata gains an optional 'delegatees' array of
{ delegate, amount }, ordered by delegate address ascending and present only
when the event has more than one row. 'delegate', 'amount' and
'previousDelegate' keep their meaning and still come from the primary row that
indexDelegationsByKey selects, so the deployed client and the dashboard feed
renderer are unaffected.
…ages (review #3676245485) "Hide dust" is on by default and enforced by the query, so an address whose every interaction is under $1 came back empty and the early return took the whole table away, footer switch included, with no way to turn the filter back off. The table now stays mounted and shows an empty state that names the filter responsible. TopInteractions owns the genuinely-no-interactions case instead: its query carries no filters, so it hides the table and shows its blank slate alone rather than stacking two empty states.
…ew #3676245492) The override was a bare entity type, so any path that cleared drawerAddress without clearing it too (the section's tab cleanup, each parent's onClose) left it behind for the next address opened from a table, which then rendered the wrong profile's tabs. It is now recorded as '<entityType>:<address>' and honored only while the recorded address matches the drawer's, compared case-insensitively since the two come from different sources. Re-pointing the drawer drops the override by itself, so no cleanup path has to remember it, and a future one cannot reintroduce the bug. Encoding lives in a single useDrawerEntityOverride hook shared by both writers; DrawerActivityFeed writes through it directly, which retires the onEntityTypeChange prop that could not carry the address.
Drop comments that restated the code they sat on: JSX section labels (Filters, Timeline, Delegators), component headers that paraphrased the component name, and a guard comment narrating its own condition. Tighten the remaining ones to the reason the code is the way it is, and drop the trailing ticket refs, which point at the PR rather than at the code. Comments only, no behavior change.
…iew) The AAVE delegates table renders `combined - balance` as "Delegation received", but the repository applied `fromValue`/`toValue` to the combined total (delegated power plus the account's own balance). A large self balance alone could satisfy a minimum, and it could push a genuinely delegated account past a maximum. Filter the delegated power expression instead, which also matches what `orderBy=votingPower` already sorts by on the same endpoint, and what every other DAO does. Also guard the Top Interactions `minAmount`/`maxAmount` parse: both come from the URL, so a stale or hand-edited `?minAmount=1.5` threw inside `BigInt()` while rendering and took the drawer down. Invalid values are now ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex review (PR 2084, round 5) plus findings from a pass over the rest of the diff looking for the same defect classes. Bound votes by `toDate` in both activity aggregates. The window holds every proposal whose voting period overlaps the range, so a proposal opened near the end stays votable after it; a vote cast later was counting as activity inside a period that closed before the vote existed. The bound goes in the LEFT JOIN's ON clause, never in WHERE, so the proposal is still listed with no vote attached, which also keeps the `no_vote` filter and the voteTiming ordering consistent. `getUserVotes` takes the same bound so the analytics (votedProposals, winRate, yesRate, avgTimeBeforeEnd) agree with the page. The banner copy, "no votes cast in the selected period", is the semantics being enforced here. The lower bound is deliberately left off: the window includes proposals that opened before the range and were still votable inside it, and a vote on one of those is real participation, so bounding below would trade this overcount for an undercount. Self-review findings: - Voting Power History let a user minimum below 1 token replace the low importance floor instead of combining with it, so sub-token rows came back while the switch still read as on. It now takes the larger of the two, and ignores an unparseable URL value rather than letting it defeat the floor. - Delegates did not validate `drawerAddress` while its sibling Token Holders tab does, so a hand-edited value opened a drawer every address query below rejects. Both use `parseAsAddress` now. - The AAVE page parsed `tab` as a plain string, so `?tab=foo` rendered Token Holders with neither button highlighted. Enum parsed, which also retires the `as TabId` cast. This one predates the PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Zero fromDate ignored
- Client hooks now pass fromDate when it is 0, and the proposals-activity service treats explicit 0 as an all-time lower bound instead of falling back to the delegate's first vote.
Or push these changes by commenting:
@cursor push 83ef507641
Preview (83ef507641)
diff --git a/apps/api/src/services/proposals-activity/index.ts b/apps/api/src/services/proposals-activity/index.ts
--- a/apps/api/src/services/proposals-activity/index.ts
+++ b/apps/api/src/services/proposals-activity/index.ts
@@ -140,7 +140,10 @@
const votingDelaySeconds = Number(votingDelay) * blockTime;
const activityStart =
- fromDate && fromDate > firstVoteTimestamp ? fromDate : firstVoteTimestamp;
+ fromDate !== undefined &&
+ (fromDate === 0 || fromDate > firstVoteTimestamp)
+ ? fromDate
+ : firstVoteTimestamp;
// Get proposals with votes, filtering, sorting, and pagination in SQL
const { proposals: proposalsWithVotes, totalCount } =
diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts
--- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts
+++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts
@@ -105,7 +105,7 @@
orderDirection,
...(orderBy ? { orderBy } : {}),
limit,
- ...(fromDate ? { fromDate } : {}),
+ ...(fromDate !== undefined ? { fromDate } : {}),
...(toDate ? { toDate } : {}),
...(address ? { addresses: [address] } : {}),
...(fromValue ? { fromValue } : {}),
@@ -169,7 +169,7 @@
daoId.toLowerCase() as ProposalsActivityPathParamsDaoEnumKey,
{
address: addr,
- ...(fromDate ? { fromDate } : {}),
+ ...(fromDate !== undefined ? { fromDate } : {}),
// Both bounds, so "Voted X/Y" counts the selected window
// instead of everything up to today.
...(toDate ? { toDate } : {}),
diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegatesActivity.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegatesActivity.ts
--- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegatesActivity.ts
+++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegatesActivity.ts
@@ -73,7 +73,7 @@
daoId.toLowerCase() as ProposalsActivityPathParamsDaoEnumKey,
{
address: addr,
- ...(fromDate ? { fromDate } : {}),
+ ...(fromDate !== undefined ? { fromDate } : {}),
// Both bounds, so "Voted X/Y" counts the selected window
// instead of everything up to today.
...(toDate ? { toDate } : {}),You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit d19b3d8. Configure here.
| // when voting opens rather than on when the proposal was created. | ||
| const votingPeriodSeconds = | ||
| Number(votingPeriodBlocks + votingDelay) * blockTime; | ||
| const votingDelaySeconds = Number(votingDelay) * blockTime; |
There was a problem hiding this comment.
Zero fromDate ignored
Medium Severity
With the MAX period, stakeholders pass fromDate as 0 for an all-time window, but client hooks only attach fromDate when it is truthy, so 0 never reaches the API. The activity service also treats 0 like missing input and uses the delegate’s first vote as the window start, so proposal activity totals under MAX no longer match the intended all-time range used elsewhere on the page.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit d19b3d8. Configure here.
|
🚅 Deployed to the anticapture-pr-2104 environment in anticapture-infra
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d19b3d8b82
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
UI Review — Dev → Main promotion
This is a 135-file dev→main promotion accumulating several already-individually-reviewed PRs (Holders & Delegates v3, AAVE amount filters, proposal description limit, ENS/Compound Security Council copy). Since squashing/rebasing this many commits is exactly where things quietly regress, this review focused on the accumulated, actually-shipping surface: the Stakeholders rename + redirects, the new default tab, and the Security Council card — rather than re-litigating each already-shipped feature. Stakeholders section (rename from Holders & Delegates)Verified — no issues found (checked because this is the highest-regression-risk surface in a squash promotion):
Delegates-as-default-tab:
Security Council cardVerified — no issues found:
Nice-to-have — copy consistency question:
AAVE Delegates table (amount filter + row borders)Verified — no issues found:
Create-proposal description limit (100,000 chars)Verified — no issues found: Mobile
Summary0 must-fix, 2 nice-to-have (both "question for author" style, not blocking):
Everything else checked (rename completeness, redirects, tab defaults, Security Council card, AAVE table changes, proposal description limit, mobile e2e coverage) came back clean. Generated by Claude Code |
Data inconsistency reports
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
🔍 Vercel preview: https://anticapture-d8sl8swrs-ful.vercel.app |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c187559c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
chore: version packages
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1105b3e1d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".



Note
Medium Risk
Changes SQL semantics for delegate activity and inactive VP across date windows; incorrect bounds could mislabel delegates inactive. New endpoints and feed filters are additive, but AAVE filter behavior is a breaking fix for clients that relied on combined VP ranges.
Overview
Holders & Delegates v3 adds API support for inactive delegated VP (
GET /voting-powers/inactive-summary), former delegators (GET /accounts/:address/delegators/historical), and per-DAO treasury/vesting labels (GET /addresses/labels). Proposal activity gains optionaltoDateand stricter window rules: proposals count when voting opens (creation + voting delay), and votes only count if cast inside[fromDate, toDate]. Feed acceptsrelevance=ALL, optionaladdressfiltering, and optionaldelegateeson split delegations. AAVEfromValue/toValueon voting powers now filter delegated power only.The dashboard renames the section to Stakeholders (
/stakeholderswith redirects fromholders-and-delegates), defaults the Delegates tab, and extends AAVE delegate tables with amount filters and row borders. Create-proposal description limit rises to 100,000 characters with pre-save validation. DAO overview updates Security Council copy (configurable label, ENS July 2026 council per changeset) and links point at the new stakeholders routes.Heavy unit test coverage backs former-delegator SQL, inactive-summary, proposals-activity date bounds, and feed address/split-delegation behavior.
Reviewed by Cursor Bugbot for commit d19b3d8. Configure here.