Skip to content

Dev - #2104

Open
pikonha wants to merge 66 commits into
mainfrom
dev
Open

Dev#2104
pikonha wants to merge 66 commits into
mainfrom
dev

Conversation

@pikonha

@pikonha pikonha commented Aug 4, 2026

Copy link
Copy Markdown
Member

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 optional toDate and stricter window rules: proposals count when voting opens (creation + voting delay), and votes only count if cast inside [fromDate, toDate]. Feed accepts relevance=ALL, optional address filtering, and optional delegatees on split delegations. AAVE fromValue/toValue on voting powers now filter delegated power only.

The dashboard renames the section to Stakeholders (/stakeholders with redirects from holders-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.

pikonha and others added 30 commits July 22, 2026 19:36
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>
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

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.

Create PR

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d19b3d8. Configure here.

@railway-app

railway-app Bot commented Aug 4, 2026

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2104 environment in anticapture-infra

Service Status Web Updated (UTC)
ens-indexer-offchain ✅ Success (View Logs) Aug 5, 2026 at 9:15 pm
gitcoin-indexer-offchain ✅ Success (View Logs) Aug 5, 2026 at 9:14 pm
uniswap-indexer-offchain ✅ Success (View Logs) Aug 5, 2026 at 9:14 pm
compound-indexer-offchain ✅ Success (View Logs) Aug 5, 2026 at 9:13 pm
shutter-indexer-offchain ✅ Success (View Logs) Aug 5, 2026 at 6:03 pm
fluid-api ✅ Success (View Logs) Aug 4, 2026 at 7:36 pm
gitcoin-api ✅ Success (View Logs) Aug 4, 2026 at 7:36 pm
tornado-api ✅ Success (View Logs) Aug 4, 2026 at 7:36 pm
uniswap-api ✅ Success (View Logs) Aug 4, 2026 at 7:35 pm
obol-api ✅ Success (View Logs) Aug 4, 2026 at 7:35 pm
lil-nouns-api ✅ Success (View Logs) Aug 4, 2026 at 7:34 pm
authful ✅ Success (View Logs) Web Aug 4, 2026 at 7:33 pm
ens-api ✅ Success (View Logs) Aug 4, 2026 at 7:33 pm
compound-api ✅ Success (View Logs) Aug 4, 2026 at 7:33 pm
address-enrichment ✅ Success (View Logs) Web Aug 4, 2026 at 7:33 pm
scroll-api ✅ Success (View Logs) Aug 4, 2026 at 7:32 pm
otelcol ✅ Success (View Logs) Aug 4, 2026 at 7:32 pm
nouns-api ✅ Success (View Logs) Aug 4, 2026 at 7:31 pm
aave-api ✅ Success (View Logs) Aug 4, 2026 at 7:30 pm
prometheus ✅ Success (View Logs) Aug 4, 2026 at 7:30 pm
alertmanager ✅ Success (View Logs) Web Aug 4, 2026 at 7:30 pm
shutter-api ✅ Success (View Logs) Aug 4, 2026 at 7:30 pm
mcp ✅ Success (View Logs) Web Aug 4, 2026 at 7:29 pm
ens-relayer ✅ Success (View Logs) Aug 4, 2026 at 7:29 pm
gateful ✅ Success (View Logs) Web Aug 4, 2026 at 7:29 pm
tempo ✅ Success (View Logs) Aug 4, 2026 at 7:28 pm
grafana ✅ Success (View Logs) Web Aug 4, 2026 at 7:28 pm
loki ✅ Success (View Logs) Aug 4, 2026 at 7:27 pm
docs ✅ Success (View Logs) Web Aug 4, 2026 at 7:27 pm
aave-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
tornado-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
lil-nouns-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
scroll-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
ens-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
compound-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
obol-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:47 pm
gitcoin-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
uniswap-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
shutter-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
fluid-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
nouns-indexer ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
erpc ✅ Success (View Logs) Web Aug 4, 2026 at 5:46 pm
nodeful ✅ Success (View Logs) Aug 4, 2026 at 5:46 pm
user-api ✅ Success (View Logs) Web Aug 4, 2026 at 5:42 pm

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/api/src/repositories/delegations/former-delegators.ts

Copy link
Copy Markdown
Collaborator

UI Review — Dev → Main promotion

⚠️ Reviewed without a Figma reference — preview + UX-expert evaluation. No figma.com link in the PR body, in ClickUp DEV-562 ("Holders & Delegates v3"), in ClickUp DEV-476 ("value filters"), or in a workspace search for "Security Council" / "proposal description limit" / "Stakeholders rename". DEV-562 does carry ~25 ClickUp mockup screenshots, but they could not be downloaded in this session (the sandbox's outbound proxy rejects *.clickup-attachments.com), so this review leans on the ticket's written spec plus direct code/diff analysis. The Vercel preview (https://anticapture-r1w5cq4mz-ful.vercel.app, found via the Deploy Vercel (dev) Action's job logs — no Vercel bot comment was posted on this PR) returned 403 (deployment protection), so no live-page confirmation was possible either. Findings below are code-grounded, not preview-grounded.

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):

  • All user-facing copy is renamed consistently: page titles/metadata, OG images (stakeholders/opengraph-image.tsx for both [daoId] and whitelabel), desktop sidebar (HeaderDAOSidebar.tsx), mobile nav select (HeaderNavMobile.tsx), sitemap (app/sitemap.ts), and dao-navigation.ts's FEATURE_PAGE_SET. A grep across every added line in the diff turned up zero leftover user-visible "Holders & Delegates" strings — the only surviving occurrences are internal identifiers (HoldersAndDelegatesSection component name, file paths), not rendered text.
  • Redirects are covered from three angles: next.config.ts adds permanent redirects for /holders-and-delegates, /whitelabel/:daoId/holders-and-delegates, and /:daoId/holders-and-delegates (the last also covers AAVE's static /aave/holders-and-delegates, since static segments win over [daoId]); the legacy app/[daoId]/(main)/holders-and-delegates/page.tsx and its whitelabel counterpart also self-redirect via permanentRedirect() as a fallback. e2e/holders-and-delegates.spec.ts has a dedicated redirect test.
  • TheSectionLayout's subtitle prop (previously passed as "Holders & Delegates") was removed rather than renamed — checked and confirmed it was already dead: TheSectionLayout.tsx never destructured/rendered subtitle even before this PR, so nothing disappears from the page (apps/dashboard/shared/components/containers/TheSectionLayout.tsx).

Delegates-as-default-tab:

  • [Code-only] Confirmed consistently wired: HoldersAndDelegatesSection.tsx sets DEFAULT_TAB = "delegates" and reorders TABS to put Delegates first; the standalone AAVE page (app/aave/stakeholders/page.tsx) independently defaults to "tokenHolders" (AAVE has no proposal data, so Delegates isn't necessarily the more useful default there).
  • [Code-only] [Question for author] Worth a quick confirmation with product that AAVE is intentionally the one exception to the new "Delegates first" default, since DEV-562's spec ("the 'delegates' tab should be primary") doesn't call out an AAVE carve-out.

Security Council card

Verified — no issues found:

  • Compound's new label: "Proposal Guardian" renders correctly uppercased (SecurityCouncilCard.tsx line ~64, uppercase class added alongside the dynamic {label}), and the danger-zone tooltip in ProgressBar.tsx was updated in lockstep to read daoOverview.securityCouncil?.label ?? "Security Council" — so the two copies of "which body is this" stay in sync.
  • Compound's multisig numbers are internally consistent: multisig.threshold: 5, signers: 9 in comp.ts matches the "5/9 multisig approval" and "nine signers needing five signatures" copy in the same file's currentSetting/impact/multisig.description strings.
  • ENS's updated multisig (5/8, expiring July 16 2028) is consistent between the structured securityCouncil config and the governance-implementation currentSetting prose in ens.ts.

Nice-to-have — copy consistency question:

  • [Code-only] [Question for author] apps/dashboard/shared/dao-config/comp.ts, GovernanceImplementationEnum.SECURITY_COUNCIL.nextStep: still reads "Follow L2Beat's standards to have a more secure Security Council." while every other Compound-specific reference in this same config was renamed to "Proposal Guardian" for consistency with the new label. This may be intentional (L2Beat's framework is literally called "Security Council" as a proper noun), but since it sits in the same object as three freshly-renamed strings, worth a sanity check with whoever owns this copy — is this the one place the generic term should stay, or was it just missed?

AAVE Delegates table (amount filter + row borders)

Verified — no issues found:

  • AmountFilter (shared/components/design-system/table/filters/amount-filter/AmountFilter.tsx) is a pre-existing DS component (already used by the shared Delegates.tsx table before this PR) — its reuse here in app/aave/stakeholders/DelegationTable.tsx is not a hand-rolled bypass.
  • withRowBorders is a proper new Table DS prop (shared/components/design-system/table/Table.tsx), not a one-off className hack, and its pseudo-element implementation is deliberately documented in-line (border-separate tables don't paint <tr> borders).
  • border-light-dark, bg-tangerine/--color-tangerine, bg-middle-dark etc. used throughout the new/changed table and switcher code all resolve to real tokens in apps/dashboard/app/globals.css — no invented utility classes.

Create-proposal description limit (100,000 chars)

Verified — no issues found: BODY_CHAR_LIMIT/BODY_WARNING_THRESHOLD (10k→100k / 9.5k→95k), the zod schema max + message, the character counter (now .toLocaleString()-formatted on both sides of the /, so it reads "12,345 / 100,000" instead of "12345 / 100,000"), and the pre-save guards added to both handleShare/handleSaveDraft in ProposalCreationForm.tsx (blocking the request client-side with a toast instead of letting the drafts endpoint 500) are all wired to the same constant and stay in sync.

Mobile

  • e2e/mobile-smoke.spec.ts and e2e/holders-and-delegates.spec.ts were both updated to assert the renamed "Stakeholders" heading and the new default tab — good coverage for the two things most likely to silently break on mobile in a rename this size.
  • HeaderNavMobile.tsx's page-select options correctly source PAGES_CONSTANTS.holdersAndDelegates.navTitle (= "Stakeholders"), matching desktop.
  • [Code-only] Not a regression, just flagging for awareness: the new SwitcherDateRange.tsx (shared/components/switchers/SwitcherDateRange.tsx, lines 173, 190, 195) hardcodes dark-only colors in its mobile dropdown (bg-[#26262A], border-white/10 bg-[#1C1C1F], hover:bg-[#26262A]) instead of theme tokens like bg-surface-contrast/border-border-contrast used elsewhere in the same file. Whitelabel routes render without the .dark class (apps/dashboard/app/layout.tsx: className={isWhitelabel ? undefined : "dark"}), so in principle this dropdown could show low-contrast (dark box, text-primary dark text in light theme) on a whitelabel DAO's mobile Stakeholders page. However, this exact hex pattern already exists pre-PR in SwitcherDate.tsx, SwitcherChart.tsx, Dropdown.tsx, and DaoInfoDropdown.tsx — so this is inherited convention, not something introduced by this PR, and not something to hold this PR for. Mentioning only because it's now propagated into one more surface on the very page this PR is renaming/promoting.

Summary

0 must-fix, 2 nice-to-have (both "question for author" style, not blocking):

  1. AAVE's Delegates-tab-is-default rule doesn't extend to the standalone /aave/stakeholders page (still defaults to Token Holders) — confirm intentional.
  2. comp.ts's SECURITY_COUNCIL.nextStep still says "Security Council" instead of "Proposal Guardian" — confirm intentional (L2Beat's framework name) vs. missed rename.

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

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview Aug 4, 2026 7:28pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture (dev) Ignored Ignored Aug 4, 2026 7:28pm

Request Review

@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2104 August 4, 2026 18:24 Destroyed
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / dev August 4, 2026 18:24 Inactive
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-d8sl8swrs-ful.vercel.app

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/dashboard/app/api/report/route.ts
Comment thread apps/dashboard/app/aave/stakeholders/page.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants