Skip to content

Dev - #2088

Merged
pikonha merged 34 commits into
mainfrom
dev
Jul 27, 2026
Merged

Dev#2088
pikonha merged 34 commits into
mainfrom
dev

Conversation

@pikonha

@pikonha pikonha commented Jul 27, 2026

Copy link
Copy Markdown
Member

Note

Medium Risk
Touches auth credential boundaries (usage-only vs provisioning keys), new DB migrations in Authful, and wallet RPC routing—misconfiguration could break sign-in or RPC until env vars are set correctly.

Overview
Adds an end-to-end 30-day daily request chart for self-service API keys: Gateful buffers user:* traffic and flushes idempotent batches to Authful via a new usage-only credential (USAGE_API_KEY / TOKEN_SERVICE_USAGE_API_KEY) that cannot mint or revoke; Authful persists token_usage_daily with tenant-scoped read/write rules; User API exposes GET /me/api-keys/usage; the dashboard renders a filterable stacked bar chart and related API Keys UX (docs link, per-row delete, agent commands in the save modal).

Dashboard wallet RPC no longer uses NEXT_PUBLIC_ALCHEMY_KEY—wagmi routes through a same-origin /api/rpc/<chainId> proxy that injects ERPC_SECRET server-side and forwards trusted client IP for per-IP limits.

DAO API proposal-activity queries now exclude canceled proposals (including first-vote timestamp alignment). Indexer corrects ENS token startBlock. eRPC configs gain dashboard integration docs, trusted forwarders, failsafe/rate-limit/cache tuning, and Chainstack fallback tagging; monitoring adds Postgres/Railway exporter docs and alert guidance. Minor panel layout and whitelabel sign-in (direct SIWE) fixes; AGENTS.md documents ClickUp issue tracker and agent skills.

Reviewed by Cursor Bugbot for commit db90edd. Configure here.

pikonha and others added 30 commits July 20, 2026 19:43
A score multiplier only re-sorts upstreams; it doesn't gate them. With
group:fallback removed, Chainstack (paid) received hedge and transient
failover traffic whenever Nodeful was merely slow or throttled, not only
when unhealthy/unsupported as the README claims. Tag each Chainstack
upstream tier:fallback so eRPC's default selection policy (auto-attached
on the tag) holds it out of rotation until no primary survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…-activity

Remove canceled proposals from the activity
- footer links now read About / Docs / Terms of Service / Give Feedback:
  About keeps the GitBook link, Docs points to https://docs.anticapture.com
- API keys page gets a "See our Docs" button next to Create key
- whitelabel sign-in skips the one-button login modal and triggers the
  SIWE flow directly; the dialog only surfaces for signing progress or
  errors, and dismissing the wallet picker cancels sign-in cleanly
Design review feedback: the row options menu held a single action and its
ellipsis trigger was too small to notice. The menu is gone; each active key
row now shows a tooltip-labeled destructive trash button that opens the same
delete confirmation modal.
…-siwe

feat(dashboard): docs links and direct whitelabel SIWE sign-in
@vercel

vercel Bot commented Jul 27, 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, Comment Jul 27, 2026 7:00pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture (dev) Ignored Ignored Jul 27, 2026 7:00pm

Request Review

@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: Wrong neverVoted for canceled-only
    • Added hasEverVoted to distinguish delegates with no votes from those whose only votes are on canceled proposals, returning neverVoted: false for the latter.

Create PR

Or push these changes by commenting:

@cursor push 4d0a29734f
Preview (4d0a29734f)
diff --git a/apps/api/src/repositories/proposals-activity/index.ts b/apps/api/src/repositories/proposals-activity/index.ts
--- a/apps/api/src/repositories/proposals-activity/index.ts
+++ b/apps/api/src/repositories/proposals-activity/index.ts
@@ -47,6 +47,15 @@
 export class DrizzleProposalsActivityRepository {
   constructor(private readonly db: Drizzle) {}
 
+  async hasEverVoted(address: Address): Promise<boolean> {
+    const [vote] = await this.db
+      .select({ timestamp: votesOnchain.timestamp })
+      .from(votesOnchain)
+      .where(eq(votesOnchain.voterAccountId, address))
+      .limit(1);
+    return !!vote;
+  }
+
   async getFirstVoteTimestamp(address: Address): Promise<number | null> {
     // Only consider votes on non-canceled proposals so activityStart matches
     // the activity scope (canceled proposals are excluded from the metrics).

diff --git a/apps/api/src/repositories/proposals-activity/index.unit.test.ts b/apps/api/src/repositories/proposals-activity/index.unit.test.ts
--- a/apps/api/src/repositories/proposals-activity/index.unit.test.ts
+++ b/apps/api/src/repositories/proposals-activity/index.unit.test.ts
@@ -76,6 +76,32 @@
     await db.delete(proposalsOnchain);
   });
 
+  describe("hasEverVoted", () => {
+    it("returns false when no votes exist for the address", async () => {
+      const result = await repository.hasEverVoted(VOTER);
+      expect(result).toBe(false);
+    });
+
+    it("returns true when votes exist only on canceled proposals", async () => {
+      await db.insert(proposalsOnchain).values(
+        createProposal({
+          id: "canceled",
+          txHash: "0xtx1",
+          status: "CANCELED",
+        }),
+      );
+      await db.insert(votesOnchain).values(
+        createVote({
+          txHash: "0xvote1",
+          proposalId: "canceled",
+        }),
+      );
+
+      const result = await repository.hasEverVoted(VOTER);
+      expect(result).toBe(true);
+    });
+  });
+
   describe("getFirstVoteTimestamp", () => {
     it("returns null when no votes exist for the address", async () => {
       const result = await repository.getFirstVoteTimestamp(VOTER);

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
@@ -65,6 +65,8 @@
 }
 
 export interface ProposalsActivityRepository {
+  hasEverVoted(address: Address): Promise<boolean>;
+
   getFirstVoteTimestamp(address: Address): Promise<number | null>;
 
   getProposals(
@@ -116,7 +118,8 @@
       await this.repository.getFirstVoteTimestamp(address);
 
     if (!firstVoteTimestamp) {
-      return this.createEmptyActivity(address, true);
+      const hasEverVoted = await this.repository.hasEverVoted(address);
+      return this.createEmptyActivity(address, !hasEverVoted);
     }
 
     // Get voting period for the DAO from blockchain

diff --git a/apps/api/src/services/proposals-activity/index.unit.test.ts b/apps/api/src/services/proposals-activity/index.unit.test.ts
--- a/apps/api/src/services/proposals-activity/index.unit.test.ts
+++ b/apps/api/src/services/proposals-activity/index.unit.test.ts
@@ -20,6 +20,7 @@
 
 function createStubRepo(): ProposalsActivityRepository & {
   lastActivityStart: number | null;
+  hasEverVotedValue: boolean;
   firstVoteTs: number | null;
   proposals: DbProposal[];
   votes: DbVote[];
@@ -27,11 +28,13 @@
 } {
   const stub = {
     lastActivityStart: null as number | null,
+    hasEverVotedValue: false,
     firstVoteTs: null as number | null,
     proposals: [] as DbProposal[],
     votes: [] as DbVote[],
     paginationResult: { proposals: [] as DbProposalWithVote[], totalCount: 0 },
 
+    hasEverVoted: async (_address: Address) => stub.hasEverVotedValue,
     getFirstVoteTimestamp: async (_address: Address) => stub.firstVoteTs,
     getProposals: async (
       _daoId: DaoIdEnum,
@@ -147,6 +150,24 @@
       });
     });
 
+    it("should return empty activity with neverVoted false when votes are only on canceled proposals", async () => {
+      repo.hasEverVotedValue = true;
+      repo.firstVoteTs = null;
+
+      const result = await service.getProposalsActivity(defaultRequest);
+
+      expect(result).toEqual({
+        address: VOTER_ADDRESS,
+        totalProposals: 0,
+        votedProposals: 0,
+        neverVoted: false,
+        winRate: 0,
+        yesRate: 0,
+        avgTimeBeforeEnd: 0,
+        proposals: [],
+      });
+    });
+
     it("should return proposals with user votes and analytics", async () => {
       repo.firstVoteTs = 1699000000;
       repo.paginationResult = {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit db90edd. Configure here.

Comment thread apps/api/src/repositories/proposals-activity/index.ts

@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: db90edd16d

ℹ️ 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/gateful/src/auth/usage.ts

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown
Contributor

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

@pikonha
pikonha merged commit 67a6db5 into main Jul 27, 2026
66 of 67 checks passed
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.

3 participants