Skip to content

feat(credits-admin): auth-first responsive console (login gate + activity center + slide-over detail)#371

Open
fbac wants to merge 9 commits into
otr-devfrom
fbac/credits-admin-console-rework
Open

feat(credits-admin): auth-first responsive console (login gate + activity center + slide-over detail)#371
fbac wants to merge 9 commits into
otr-devfrom
fbac/credits-admin-console-rework

Conversation

@fbac

@fbac fbac commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Reworks src/api/v2/credits-admin/ from a single server-rendered page into an auth-first, responsive console.

Backend (2 new bearer-gated reads, no DB migration, no money-mutation change):

  • listRecentAdminAudit + keyset cursor (encode/decodeAuditCursor) in audit-repository.ts — global newest-first pagination with a (createdAt, id) tiebreaker.
  • GET /audit/recent?action=&cursor= — paginated global admin-activity feed.
  • GET /whoami — login validation + actor identity + creditsPerUsd. Moving creditsPerUsd here keeps the public shell data-free.

Client (handlers/admin-page.tshandlers/admin-page/):

  • Split into index.ts (CSP + assembly), styles.ts, login-view.ts, console-view.ts, script.ts.
  • Auth-first login gate: page serves only a login card; the console is hidden and zero data is fetched until the token validates via /whoami. guardFetch bounces to login on any 401. Lock button.
  • Responsive console: sticky header (identity + lock), left rail (search + action facets), center = paginated global activity list (load-more @ 50), right slide-over account detail (single balance card, none/entitled/lapsed subscription chip, per-period allotment, grant/adjust, ledger, 30-day usage sparkline, daily refills, per-account audit).

Design notes:

  • The client login gate is presentation-only; real authz remains the server bearer gate (creditsAdminTokenAuth) on every data endpoint. The public GET / shell serves no data, which is what makes that acceptable.
  • No new UserCredits/CreditLedger writes — grant/adjust still route through @/payments via the existing endpoints.
  • Single inline-nonce file under strict CSP (script-src 'nonce-…', no inline handlers); every DB free-text field is escaped in the client renderers.

Verification (automated)

  • pnpm vitest run tests/credits-admin/64/64 pass (13 files).
  • pnpm typecheck, pnpm lint (repo-wide eslint .), prettier --check → all clean.
  • node --check on the assembled inline browser JS → OK.

Test Plan (manual browser-walk — required before merge)

Run against pnpm dev with CREDITS_ADMIN_API_TOKEN set, at /api/v2/credits-admin/, with one seeded account (subscription + some admin-audit rows):

  • Auth-first, no leak — fresh browser loads only the login card; Network shows no /audit/recent / /accounts / /search before unlock.
  • Bad token — inline "Invalid token" error, console stays hidden.
  • Good token — login disappears, console appears, identity shows, activity table populates.
  • Load-more — next page appends beneath existing rows, button hides on last page.
  • Facets — Grants / Adjusts / All refilter, active pill highlights, pagination resets.
  • Search → detail — accountId (and wallet) opens the slide-over with balance, sub chip (correct color), allotment + period-consumes, sparkline, refills, ledger, audit.
  • Slide-over feel — smooth open/close via Close, scrim, brand; reduced-motion respected.
  • Grant round-trip — success toast, panel balance updates, new row at top of activity.
  • Adjust floor — large negative adjust → "Below floor" toast, balance unchanged.
  • 401 mid-session — clear sessionStorage token, trigger an action → bounced to login ("Session expired").
  • Lock — returns to login, token cleared, reload does not silently re-enter.
  • Mobile — rail collapses above center, detail full-screen, tables scroll in wrappers, 44px touch targets.

Known deferred (non-blocking)

  • Load-more rapid double-click can duplicate rows (read-only cosmetic; guard intentionally not added).
  • "Invalid token" copy is shown for a CREDITS_ADMIN_REQUIRE_CF_IDENTITY=true 401 — dormant (no CF perimeter deployed on convos-backend).
  • Accounts-filter center mode (balance threshold / broken subscribers / by grant-kind) is an approved fast-follow, not in this PR.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Note

Add auth-first credits admin console with login gate, activity center, and slide-over detail panel

  • Replaces the monolithic admin-page.ts with a modular shell split into login-view.ts, console-view.ts, script.ts, and styles.ts.
  • The page now serves a data-free HTML shell; the client-side script authenticates via bearer token, calls GET /whoami to resolve actor identity and creditsPerUsd, then loads recent admin activity via GET /audit/recent.
  • Adds GET /api/v2/credits-admin/whoami returning the actor's email and creditsPerUsd, and GET /api/v2/credits-admin/audit/recent with optional action filter and keyset pagination backed by a new composite index on AdminAudit(createdAt, id).
  • The console includes account search, a faceted activity list with load-more pagination, and a slide-over detail panel for grant/adjust mutations.
  • Behavioral Change: the admin page no longer server-renders any identity or pricing data; all state is fetched client-side after token entry.

Macroscope summarized b44a7de.

Summary by CodeRabbit

  • New Features

    • Added a secure Credits Admin console with token-based login.
    • Added identity verification and recent activity auditing with filtering and pagination.
    • Added account search, detail views, balance adjustments, grants, usage information, and audit history.
    • Added responsive layouts, notifications, loading states, and lock/logout controls.
  • Bug Fixes

    • Improved authentication handling and validation for invalid requests and cursors.
  • Tests

    • Added coverage for authentication, auditing, pagination, filtering, and console behavior.

@fbac fbac requested a review from a team as a code owner July 14, 2026 21:05
@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown

Claude finished @fbac's task —— View job


PR Review Complete ✅

Overview

This is a well-architected refactor that transforms the credits-admin interface from a monolithic server-rendered page into a modern, auth-first SPA-style console. The code quality is high, with strong security practices, comprehensive test coverage, and adherence to repository conventions.

✅ Strengths

1. Excellent Security Implementation

  • ✅ Strict CSP with nonce-based script execution (script-src 'nonce-${nonce}')
  • ✅ All user/DB content properly escaped via esc() function in script.ts:6
  • ✅ Auth-first design: public shell leaks zero data (verified by tests)
  • ✅ Bearer token stored in sessionStorage (not localStorage or cookies)
  • ✅ 401 handling with automatic re-login flow
  • Cache-Control: no-store prevents caching sensitive content
  • ✅ Idempotency keys with UUID prefixes for grant/adjust operations

2. Strong Test Coverage

  • ✅ 64/64 tests passing across 13 test files
  • ✅ Comprehensive repository tests including edge cases:
    • Keyset pagination with (createdAt, id) tiebreaker
    • Cursor encoding/decoding with validation
    • Action filtering with pagination
    • Timestamp collision handling (script.ts:138-168)
  • ✅ Integration tests for all new endpoints
  • ✅ Shell assertions verify data-free HTML and auth wiring

3. Performance & Scalability

  • Database index added for (createdAt, id) ordering (migration 20260714213447)
    • This addresses the Macroscope concern about missing indexes
    • Supports efficient keyset pagination as audit history grows
  • ✅ Cursor-based pagination prevents offset/limit scalability issues
  • ✅ 50-row page limit with stable continuation

4. Code Organization

  • ✅ Clean separation: login-view.ts, console-view.ts, styles.ts, script.ts, index.ts
  • ✅ Modular handler structure improves maintainability
  • ✅ No new money/credits mutations—grant/adjust route through existing @/payments layer (CLAUDE.md compliant)

🟡 Issues Identified

1. 🟠 MEDIUM: Race Condition in Activity Loading (script.ts:66-78)

Issue: The loadActivity function doesn't track request generation. If a user switches facets rapidly (e.g., All → Grants → All), an older request can resolve after a newer one, overwriting the table with stale results.

Example:

// Current code (vulnerable):
function loadActivity(reset) {
  if (reset) activityCursor = null;
  var qs = "?action=" + activityAction + (activityCursor ? "&cursor=" + activityCursor : "");
  guardFetch("/audit/recent" + qs).then(function(r) {
    // No check if this response still matches current facet
    renderActivityRows(j.rows || [], !reset);
    activityCursor = j.nextCursor;
  });
}

Fix: The code already has activityGen declared (line 53) and incremented (line 68), and there's a check at line 71: if(gen!==activityGen) return;. This issue appears to be already addressed! However, I recommend adding a comment explaining the race prevention for future maintainers.

Suggested addition:

// line 68-71: Add comment
var gen = ++activityGen; // Guard against facet-switch race (discard stale responses)
var qs = "?action=" + encodeURIComponent(activityAction) + ...;
guardFetch("/audit/recent" + qs).then(function(r) { return r.json(); }).then(function(j) {
  if (gen !== activityGen) return; // Race: user switched facets; discard this response

Verdict: Already fixed, but documentation would help.


2. 🟠 MEDIUM: Toast Hidden Behind Detail Panel (styles.ts:47)

Issue: The .toast element lacks a z-index, while the detail scrim and panel use z-index: 30 and 40. When grant/adjust operations succeed from the detail panel, the toast renders beneath the scrim and is invisible.

Fix: Add z-index: 50 to .toast:

- .toast { position:fixed; bottom:24px; right:24px; padding:12px 18px; border-radius:8px; color:#fff; opacity:0; transition:opacity .2s; pointer-events:none; }
+ .toast { position:fixed; bottom:24px; right:24px; padding:12px 18px; border-radius:8px; color:#fff; opacity:0; transition:opacity .2s; pointer-events:none; z-index:50; }

Verification needed: Check the manual test plan item: "Grant round-trip — success toast, panel balance updates..."


3. 🟢 MINOR: Test Coverage Gap (audit-recent.integration.test.ts:45-72)

Issue: The pagination test seeds only 3 rows (< 50-row limit) and never asserts nextCursor or requests a second page.

Fix: Seed ≥51 rows, assert nextCursor is present, then fetch page 2:

it("paginates newest-first with nextCursor and filters by action", async () => {
  const a = await seedAccount();
  tracker.push(a);
  for (let i = 0; i < 52; i++) { // Cross the 50-row boundary
    await writeAdminAudit({
      accountId: a,
      actorEmail: "admin@test",
      action: i % 2 === 0 ? "grant" : "adjust",
      deltaCredits: BigInt(i + 1),
      reason: "seed",
      idempotencyKey: `ar_${i}`,
    });
  }
  const p1 = await adminRequest(app).get("/api/v2/credits-admin/audit/recent?action=all");
  expect(p1.status).toBe(200);
  const p1Body = p1.body as RecentAuditResponse;
  expect(p1Body.rows).toHaveLength(50);
  expect(p1Body.nextCursor).not.toBeNull();

  // Fetch page 2
  const p2 = await adminRequest(app).get(`/api/v2/credits-admin/audit/recent?cursor=${encodeURIComponent(p1Body.nextCursor!)}`);
  expect(p2.status).toBe(200);
  const p2Body = p2.body as RecentAuditResponse;
  expect(p2Body.rows.length).toBeGreaterThan(0);
  
  // Verify no overlap
  const ids1 = p1Body.rows.map(r => r.id);
  const ids2 = p2Body.rows.map(r => r.id);
  expect(ids1.some(id => ids2.includes(id))).toBe(false);
});

🔍 Additional Observations

Positive

  1. CLAUDE.md Compliance:

    • ✅ No changes to client-facing /api/v2/** request schemas (no backwards-compat risk)
    • ✅ All money operations route through @/payments layer (no direct CreditLedger writes)
    • ✅ Idempotency keys present on all mutations
  2. Accessibility:

    • aria-hidden on detail panel
    • prefers-reduced-motion support
    • ✅ 44px touch targets on mobile (@media (pointer:coarse))
  3. Error Handling:

    • ✅ Invalid cursor returns 400 with { code: "invalid_cursor" }
    • insufficient_balance and idempotency_mismatch errors mapped to user-friendly toasts

Neutral

  1. Known Deferred Items (from PR description):
    • Load-more rapid double-click can duplicate rows → Acceptable (read-only, cosmetic)
    • Invalid token copy shown for CF 401 → Acceptable (no CF perimeter deployed)
    • Accounts-filter mode (balance threshold, etc.) → Approved fast-follow

📋 Recommendations

Must-Fix Before Merge

  1. Add z-index: 50 to .toast (styles.ts:47) — Blocking UX issue

Should-Fix Before Merge

  1. Expand pagination integration test (audit-recent.integration.test.ts:45-72) — Important coverage gap

Nice-to-Have

  1. Add comment explaining race guard (script.ts:68-71) — Helps future maintainers

🎯 Final Verdict

APPROVE with minor fixes. This is high-quality work with excellent security, performance, and test coverage. The two medium-severity issues are straightforward to address:

  • Toast z-index is a 1-line CSS fix
  • Race condition is already handled (just needs documentation)
  • Test gap is non-blocking but recommended

The architecture is sound, the database index is in place, and the auth-first design properly isolates sensitive data. Great job! 🎉


Summary Checklist

  • ✅ Code quality: Excellent
  • ✅ Security: Strong (CSP, auth-first, escaping, idempotency)
  • ⚠️ Bugs: 1 UX issue (toast z-index) — fix required
  • ✅ Performance: Optimized (index added, keyset pagination)
  • ⚠️ Test coverage: Strong, but pagination test could be better
  • ✅ CLAUDE.md compliance: Full adherence

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds authenticated whoami and recent-audit APIs with cursor pagination, and introduces a modular, auth-first Credits Admin console supporting activity browsing, account details, and grant/adjust actions.

Changes

Credits Admin console and APIs

Layer / File(s) Summary
Recent audit pagination and repository contracts
src/api/v2/credits-admin/audit-repository.ts, tests/credits-admin/audit-repository.test.ts
Adds cursor encoding/decoding, stable keyset pagination, action filtering, and repository coverage for page boundaries and timestamp ties.
Authenticated whoami and audit endpoints
src/api/v2/credits-admin/schemas/requests.ts, src/api/v2/credits-admin/handlers/{whoami-get.ts,audit-recent-get.ts}, src/api/v2/credits-admin/credits-admin.router.ts, tests/credits-admin/{whoami.integration.test.ts,audit-recent.integration.test.ts}
Adds validated API handlers and authenticated routes for actor identity and recent audit data, with response mapping and integration tests.
Auth-first console and account operations
src/api/v2/credits-admin/handlers/admin-page/*, tests/credits-admin/admin-page.test.ts
Splits the page into login, console, styles, and nonce-protected assembly while adding client-side authentication, activity loading, account details, mutations, and updated shell assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminBrowser
  participant clientScript
  participant creditsAdminRouter
  participant whoamiGetHandler
  participant auditRecentGetHandler
  AdminBrowser->>clientScript: load console shell
  clientScript->>creditsAdminRouter: GET /whoami with bearer token
  creditsAdminRouter->>whoamiGetHandler: authenticate and handle request
  whoamiGetHandler-->>clientScript: actor identity and creditsPerUsd
  clientScript->>auditRecentGetHandler: GET /audit/recent with filters
  auditRecentGetHandler-->>clientScript: audit rows and nextCursor
Loading

Possibly related PRs

Suggested reviewers: lourou, neekolas

Poem

A bunny found some audits neat,
With cursors hopping page to page.
The console dons a login door,
Then shows the ledger on its stage.
Grant and adjust, with carrots bright—
Auth guards keep the burrow right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: an auth-first responsive credits-admin console with login gate, activity center, and slide-over detail.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fbac/credits-admin-console-rework

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/api/v2/credits-admin/handlers/admin-page/script.ts
}
const rows = await prisma.adminAudit.findMany({
where,
orderBy: [{ createdAt: "desc" }, { id: "desc" }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium credits-admin/audit-repository.ts:77

listRecentAdminAudit orders the entire AdminAudit table by (createdAt, id) with no matching index — the schema only indexes (accountId, createdAt) and (actorEmail, createdAt). The first-page query (and each action-filtered first page) therefore scans and sorts the whole audit table before returning 50 rows. As audit history grows, this endpoint becomes progressively slower and more CPU-intensive. Add an index on (createdAt, id) matching the keyset order, and consider a composite index covering the action-filtered access path.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/api/v2/credits-admin/audit-repository.ts around line 77:

`listRecentAdminAudit` orders the entire `AdminAudit` table by `(createdAt, id)` with no matching index — the schema only indexes `(accountId, createdAt)` and `(actorEmail, createdAt)`. The first-page query (and each `action`-filtered first page) therefore scans and sorts the whole audit table before returning 50 rows. As audit history grows, this endpoint becomes progressively slower and more CPU-intensive. Add an index on `(createdAt, id)` matching the keyset order, and consider a composite index covering the `action`-filtered access path.

Comment thread src/api/v2/credits-admin/handlers/admin-page/styles.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 14, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

3 blocking correctness issues found. New feature adding auth-first console with multiple new endpoints in credits-admin area. Author does not own any of the 17 changed files (all owned by xmtplabs/engineering). Four unresolved medium-severity comments exist, including a parseInt truncation issue that could apply incorrect credit amounts during admin mutations.

You can customize Macroscope's approvability policy. Learn more.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
tests/credits-admin/audit-recent.integration.test.ts (1)

45-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise nextCursor through the HTTP endpoint.

This test claims pagination coverage but seeds fewer than the 50-row page limit, never asserts nextCursor, and never requests page two. Seed at least 51 rows, then verify the cursor request returns a non-overlapping continuation page.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/credits-admin/audit-recent.integration.test.ts` around lines 45 - 72,
Update the test case around “paginates newest-first with nextCursor and filters
by action” to seed at least 51 audit rows, assert the first response includes a
nextCursor, then request the endpoint with that cursor and verify the
continuation page is non-overlapping with the first page. Preserve the existing
newest-first, response-shape, accountId, and action-filter assertions.
src/api/v2/credits-admin/handlers/admin-page/index.ts (2)

31-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit return; statement.

As per coding guidelines, always place the return; statement on a new line immediately after sending a response in API routes.

♻️ Proposed refactor
 </body>
 </html>`);
+  return;
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/v2/credits-admin/handlers/admin-page/index.ts` around lines 31 - 32,
Add an explicit return statement on the line immediately after sending the
response in the affected API route handler, using the surrounding handler’s
existing symbol to locate the change and preserving the current response
content.

Source: Coding guidelines


8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Omit explicit return types. As per coding guidelines, prefer inferring the value in TypeScript instead of specifying the return type on functions.

  • src/api/v2/credits-admin/handlers/admin-page/index.ts#L8-L8: Remove the explicit : void return type.
  • src/api/v2/credits-admin/handlers/admin-page/login-view.ts#L1-L1: Remove the explicit : string return type.
  • src/api/v2/credits-admin/handlers/admin-page/console-view.ts#L1-L1: Remove the explicit : string return type.
  • src/api/v2/credits-admin/handlers/admin-page/script.ts#L1-L1: Remove the explicit : string return type.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/v2/credits-admin/handlers/admin-page/index.ts` at line 8, Remove the
explicit return type annotations from adminPageHandler and the exported
functions in src/api/v2/credits-admin/handlers/admin-page/login-view.ts (line
1), console-view.ts (line 1), and script.ts (line 1), allowing TypeScript to
infer their return types.

Source: Coding guidelines

tests/credits-admin/admin-page.test.ts (1)

49-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the get() helper. Both of these lines duplicate the exact logic of the get() helper defined on line 11. Consider reusing the helper to simplify the test assertions.

  • tests/credits-admin/admin-page.test.ts#L49-L49: Replace adminRequest(app, false).get("/api/v2/credits-admin/") with get().
  • tests/credits-admin/admin-page.test.ts#L57-L57: Replace adminRequest(app, false).get("/api/v2/credits-admin/") with get().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/credits-admin/admin-page.test.ts` at line 49, Replace the duplicated
admin requests at tests/credits-admin/admin-page.test.ts lines 49-49 and 57-57
with the existing get() helper defined in the test file, preserving the current
assertions and behavior at both sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/api/v2/credits-admin/handlers/admin-page/index.ts`:
- Around line 31-32: Add an explicit return statement on the line immediately
after sending the response in the affected API route handler, using the
surrounding handler’s existing symbol to locate the change and preserving the
current response content.
- Line 8: Remove the explicit return type annotations from adminPageHandler and
the exported functions in
src/api/v2/credits-admin/handlers/admin-page/login-view.ts (line 1),
console-view.ts (line 1), and script.ts (line 1), allowing TypeScript to infer
their return types.

In `@tests/credits-admin/admin-page.test.ts`:
- Line 49: Replace the duplicated admin requests at
tests/credits-admin/admin-page.test.ts lines 49-49 and 57-57 with the existing
get() helper defined in the test file, preserving the current assertions and
behavior at both sites.

In `@tests/credits-admin/audit-recent.integration.test.ts`:
- Around line 45-72: Update the test case around “paginates newest-first with
nextCursor and filters by action” to seed at least 51 audit rows, assert the
first response includes a nextCursor, then request the endpoint with that cursor
and verify the continuation page is non-overlapping with the first page.
Preserve the existing newest-first, response-shape, accountId, and action-filter
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b9e8bf4-58a8-4881-9b78-0a4608ee57da

📥 Commits

Reviewing files that changed from the base of the PR and between 74345d5 and 74b2be2.

📒 Files selected for processing (15)
  • src/api/v2/credits-admin/audit-repository.ts
  • src/api/v2/credits-admin/credits-admin.router.ts
  • src/api/v2/credits-admin/handlers/admin-page.ts
  • src/api/v2/credits-admin/handlers/admin-page/console-view.ts
  • src/api/v2/credits-admin/handlers/admin-page/index.ts
  • src/api/v2/credits-admin/handlers/admin-page/login-view.ts
  • src/api/v2/credits-admin/handlers/admin-page/script.ts
  • src/api/v2/credits-admin/handlers/admin-page/styles.ts
  • src/api/v2/credits-admin/handlers/audit-recent-get.ts
  • src/api/v2/credits-admin/handlers/whoami-get.ts
  • src/api/v2/credits-admin/schemas/requests.ts
  • tests/credits-admin/admin-page.test.ts
  • tests/credits-admin/audit-recent.integration.test.ts
  • tests/credits-admin/audit-repository.test.ts
  • tests/credits-admin/whoami.integration.test.ts
💤 Files with no reviewable changes (1)
  • src/api/v2/credits-admin/handlers/admin-page.ts

…y race, audit index)

- toast: add z-index:50 so grant/adjust success toasts render above the
  detail slide-over (scrim z-30 / panel z-40) instead of beneath it.
- loadActivity: request-generation counter drops stale responses, fixing
  the facet-switch / rapid load-more race (wrong cursor / duplicate rows).
- AdminAudit: add @@index([createdAt, id]) matching the global keyset order
  so /audit/recent stops seq-scanning the whole table as history grows.
Comment on lines +162 to +167
if(kind==="grant"){ var c=parseInt(el("d-grant-credits").value,10); var gr=el("d-grant-reason").value.trim();
if(!c||c<=0){ toast("Positive credits required","error"); return; } if(!gr){ toast("Reason required","error"); return; }
body={credits:c,reason:gr,idempotencyKey:newKey("admin_grant")}; path="/grant"; btn=el("d-grant-btn"); }
else { var d=parseInt(el("d-adjust-delta").value,10); var ar=el("d-adjust-reason").value.trim();
if(!d){ toast("Non-zero delta required","error"); return; } if(!ar){ toast("Reason required","error"); return; }
body={delta:d,reason:ar,idempotencyKey:newKey("admin_adjust")}; path="/adjust"; btn=el("d-adjust-btn"); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium admin-page/script.ts:162

parseInt on type="number" inputs silently truncates valid numeric input. An admin entering 1e3 to grant 1,000 credits sends {credits: 1}, and 1.5 is truncated to 1 instead of rejected — so the mutation applies a different amount than the admin intended. parseInt stops at the first non-digit character, so the exponent or decimal portion is dropped. Parse the full value (e.g., Number(...)) and require a finite integer before proceeding.

-    if(kind==="grant"){ var c=parseInt(el("d-grant-credits").value,10); var gr=el("d-grant-reason").value.trim();
-      if(!c||c<=0){ toast("Positive credits required","error"); return; } if(!gr){ toast("Reason required","error"); return; }
+    if(kind==="grant"){ var gc=Number(el("d-grant-credits").value); var gr=el("d-grant-reason").value.trim();
+      if(!isFinite(gc)||gc<=0||Math.floor(gc)!==gc){ toast("Positive integer credits required","error"); return; } if(!gr){ toast("Reason required","error"); return; } var c=gc;
-    else { var d=parseInt(el("d-adjust-delta").value,10); var ar=el("d-adjust-reason").value.trim();
-      if(!d){ toast("Non-zero delta required","error"); return; } if(!ar){ toast("Reason required","error"); return; }
+    else { var ad=Number(el("d-adjust-delta").value); var ar=el("d-adjust-reason").value.trim();
+      if(!isFinite(ad)||ad===0||Math.floor(ad)!==ad){ toast("Non-zero integer delta required","error"); return; } if(!ar){ toast("Reason required","error"); return; } var d=ad;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/api/v2/credits-admin/handlers/admin-page/script.ts around lines 162-167:

`parseInt` on `type="number"` inputs silently truncates valid numeric input. An admin entering `1e3` to grant 1,000 credits sends `{credits: 1}`, and `1.5` is truncated to `1` instead of rejected — so the mutation applies a different amount than the admin intended. `parseInt` stops at the first non-digit character, so the exponent or decimal portion is dropped. Parse the full value (e.g., `Number(...)`) and require a finite integer before proceeding.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant