Skip to content

feat(frontend): add analytics dashboard at /dashboard/analytics - #539

Merged
zaira-bibi merged 1 commit into
mainfrom
zaira/feat/analytics-dashboard-ui
Jul 31, 2026
Merged

feat(frontend): add analytics dashboard at /dashboard/analytics#539
zaira-bibi merged 1 commit into
mainfrom
zaira/feat/analytics-dashboard-ui

Conversation

@zaira-bibi

@zaira-bibi zaira-bibi commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes: #487 — Analytics are not visible anywhere in the product UI

📚 Stacked on #533 (feat(permissions): add GET /permissions/can check endpoint). Review/merge #533 first; this PR's diff is against #533's branch. Part of the analytics epic #434.

What

Makes login activity visible in-app at /dashboard/analytics — a hand-rolled SVG bar chart plus two stat tiles, sourced from the existing GET /api/v1/analytics/login-activity read API — closing Milestone 1's last (UI) leg.

Changes

  • feat(frontend): lib/analytics — typed fetchLoginActivity(token, { days }) client mirroring lib/llm
  • feat(frontend): components/ui/BarChart — zero-dependency hand-rolled SVG bar chart primitive (shadcn recipe: themed, dark-mode-native, role="img")
  • feat(frontend): components/ui/StatCard — stat tile composed on Card
  • feat(frontend): app/dashboard/analytics — client page (fetch-once, client-side zero-fill of the sparse response into a continuous 30-day series, two derived stats from that same series, four UI states: loading / 403 / error / ready-or-empty) + route wrapper
  • feat(frontend): lib/permissionscheckPermission(token, permission) client calling feat(permissions): add GET /permissions/can check endpoint #533's GET /api/v1/permissions/can
  • feat(frontend): sidebar "Analytics" nav entry gated by that check (a canViewAnalytics boolean the dashboard layout fetches once and fails closed on error), threaded through the layout + mobile sidebar
  • test(frontend): vitest coverage for both clients, the two primitives, the page's four states + derivation helpers, and the sidebar gating

How to Test

  1. make frontend.install.dev (if needed), then make test.frontend.vitest → all green.
  2. cd frontend && bun run typecheck and make lint.frontend → clean.
  3. Manual (make backend.up.dev + make frontend.up.dev): grant a user a role with analytics.read at the global scope (make cli -- roles assign-role <user> <role>), log in → the Analytics nav entry appears and /dashboard/analytics renders the bar chart + two stat tiles.
  4. A user without analytics.read → no nav entry; direct navigation redirects to the dashboard.
  5. Sparse-data sanity: with some days missing, the chart still renders a continuous 30-day series and "Total logins" equals the sum of the visible bars.

Notes

This PR description was written with the assistance of an LLM (Claude).

@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from 9d44119 to d9abfaa Compare July 24, 2026 08:08
@zaira-bibi zaira-bibi self-assigned this Jul 24, 2026
@zaira-bibi zaira-bibi added the enhancement New feature or request label Jul 24, 2026
@zaira-bibi

Copy link
Copy Markdown
Contributor Author

@claude-review

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zaira-bibi's task in 2m 38s —— View job


Claude Code Review — Analytics dashboard

  • Read changed files & diff
  • Review correctness & logic
  • Review React patterns / frontend
  • Review test coverage
  • Post review feedback

Reviewed all 15 files against origin/zaira/feat/analytics-dashboard-s3. This is a clean, well-factored PR — the derivation logic is pure and injectable, the four page states are explicit, and test coverage is genuinely thorough (client, both primitives, the four page states, the derivation helpers, sidebar gating, and a dedicated window-consistency test). No blocking issues.

What's good 👍

  • Pure, testable derivation. buildDailySeries/summarize take an injected now and are unit-tested independently of the clock — the fake-timers window-consistency test is a nice touch.
  • Deriving stats from the windowed series, not the raw response (lines 34–49) is a subtle correctness win, and it's documented and tested.
  • Error handling matches the codebase. fetchLoginActivity mirrors lib/llm/client.ts exactly, and the data as … cast is safe because errorMiddleware throws on every non-2xx, so data is only populated on success. The 403 → forbidden branch works end-to-end.
  • Chart accessibility & robustness. role="img" + accessible name, key={d.label} (not array index), and the data.length === 0 / Math.max(1, …) guards prevent divide-by-zero and NaN geometry.
  • Correct client/server splitpage.tsx exports metadata while AnalyticsPage.tsx carries "use client"; splitting them is exactly right.
  • Permission gating on analytics.read is threaded cleanly through the layout → sidebar → mobile sidebar, with the in-page 403 as defense-in-depth for direct navigation.

Suggestions (all non-blocking)

Priority File Note
Low/Med AnalyticsPage.tsx:61-78 Fetch effect has no cancellation guard — post-unmount setState + stale-response ordering race on token change. Standard active flag suggested inline.
Low AnalyticsPage.tsx:110-119 Empty state keys off points.length; a boundary-only day (outside the chart window) yields points.length > 0 but an all-zero chart + "Total logins: 0" instead of the empty state. Suggest deciding from the windowed series.
Low (nit) AnalyticsPage.tsx:133 "Busiest day" hint shows a raw ISO date; a friendlier format would read better on the tile.

Details are in the three inline comments.

Notes

  • Diff is correctly scoped against the stacked base (feat(permissions): add GET /permissions/can check endpoint #533); generated.ts is untouched, as the description states.
  • TDD/test-coverage requirement is met — new functionality all ships with tests.
  • I did not run bun run typecheck / make test.frontend.vitest in this review; CI covers those gates.

Overall: approve-worthy pending the author's take on the suggestions (none blocking).
· zaira/feat/analytics-dashboard-ui

Comment on lines +61 to +78
const load = useCallback(async () => {
if (!token) return;
setState({ status: "loading" });
try {
const points = await fetchLoginActivity(token, { days: DAYS });
setState({ status: "ready", points });
} catch (err) {
if (err instanceof ApiRequestError && err.status === 403) {
setState({ status: "forbidden" });
} else {
setState({ status: "error", message: "We couldn't load analytics. Please try again." });
}
}
}, [token]);

useEffect(() => {
load();
}, [load]);

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.

Suggestion (Low/Medium): the fetch effect has no cancellation guard. If the component unmounts mid-request, or token changes while a request is in flight, the .then/catch still calls setState. On rapid token changes an earlier slow response can also land after a newer one and overwrite it (stale-response race). React 19 no longer warns on post-unmount setState, but the stale-overwrite is still a real ordering bug.

A small active flag closes both:

useEffect(() => {
  if (!token) return;
  let active = true;
  setState({ status: "loading" });
  fetchLoginActivity(token, { days: DAYS })
    .then((points) => {
      if (active) setState({ status: "ready", points });
    })
    .catch((err) => {
      if (!active) return;
      if (err instanceof ApiRequestError && err.status === 403) {
        setState({ status: "forbidden" });
      } else {
        setState({ status: "error", message: "We couldn't load analytics. Please try again." });
      }
    });
  return () => {
    active = false;
  };
}, [token]);

Minor for this page (token rarely changes here), so non-blocking — but it's a cheap, standard guard.

Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<StatCard title="Total logins" value={total} hint={`last ${DAYS} days`} />
<StatCard title="Busiest day" value={busiest?.value ?? "—"} hint={busiest?.label} />

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.

Nit (Low): the "Busiest day" hint renders a raw ISO date (busiest?.label2026-07-23). Consider a friendlier format (e.g. Jul 23) via Intl.DateTimeFormat / toLocaleDateString for the tile — the ISO label is fine on the chart axis where space is tight, but the stat tile has room. Purely cosmetic.

Comment thread frontend/app/dashboard/layout.tsx Outdated
Comment thread frontend/components/AppSidebar.tsx Outdated
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from d9abfaa to 302982c Compare July 27, 2026 07:01
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch 2 times, most recently from 8705fe8 to d9ba0f2 Compare July 27, 2026 12:11
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from d9ba0f2 to 7ead835 Compare July 27, 2026 12:56
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from 7ead835 to a49eee8 Compare July 28, 2026 07:01
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from a49eee8 to 863ac4b Compare July 28, 2026 08:00
@zaira-bibi

Copy link
Copy Markdown
Contributor Author

@claude-review

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zaira-bibi's task in 2m 7s —— View job


Claude Code Review — Analytics dashboard (re-review)

  • Read changed files & diff
  • Check status of prior review comments (mine + @abdulrafey1's)
  • Review correctness, React patterns, tests
  • Post review feedback

Re-reviewed all 19 files against origin/zaira/feat/analytics-dashboard-s3. Still a clean, well-factored PR. The refactor since the last round addresses both of @abdulrafey1's comments — nav gating now goes through the new /permissions/can endpoint rather than a permissions array on the user object. No blocking issues.

Previous review comments

Thread Status
@abdulrafey1layout.tsx "do we still need this?" (on permissions: user?.permissions) Addressed — that line is gone; the layout now fetches a single canViewAnalytics boolean via checkPermission(token, "analytics.read") (layout.tsx:77-93).
@abdulrafey1AppSidebar.tsx "Should this check not be moved to the new permissions/can endpoint" Addressed — the sidebar no longer does user?.permissions?.includes("analytics.read"); it reads the user?.canViewAnalytics boolean derived from the /permissions/can call (AppSidebar.tsx:235).
🤖 me — fetch effect cancellation guard (AnalyticsPage.tsx) 🟡 Still open (non-blocking) — see below. The layout's permission useEffect did pick up an active guard (layout.tsx:81,90) 👍, but AnalyticsPage's load still doesn't.
🤖 me — empty state keys off raw response, not windowed series 🟡 Still open (non-blocking) — AnalyticsPage.tsx:109,116 still branch on state.points.length.
🤖 me — "Busiest day" hint renders a raw ISO date 🟡 Still open (nit) — AnalyticsPage.tsx:132 still hint={busiest?.label}.

I have not auto-resolved @abdulrafey1's threads — I'll leave that to a human reviewer, but from the code they're both satisfied.

What's good 👍

  • The permission-check refactor is the right callcheckPermission (lib/permissions/client.ts) mirrors lib/analytics / lib/llm exactly, is fully typed off the generated PermissionCheckResponse schema, fails closed (data?.allowed ?? false), and the layout useEffect guards against the post-unmount / stale-token race with an active flag. Clean.
  • Fail-closed gating end-to-endcanViewAnalytics starts false, only flips true on an explicit allowed, and the .catch logs + hides. Threaded cleanly through layout → AppSidebar → MobileSidebar.
  • Windowed derivation is still pure, injectable, and tested independently of the clock (buildDailySeries/summarize), with the fake-timers window-consistency test proving the boundary-day case.
  • Test coverage meets the TDD bar — both clients, both primitives, the four page states + helpers, and the sidebar gating (canViewAnalytics true/false) all ship with tests.

Suggestions (all non-blocking, unchanged from last round)

Priority File Note
Low/Med AnalyticsPage.tsx:62-79 load still has no cancellation guard. Since the layout now owns the active-flag pattern, mirroring it here would make the two consistent. On this page token rarely changes, so still minor. Fix this →
Low AnalyticsPage.tsx:109,116 Empty state branches on state.points.length; a boundary-only day (today - DAYS, outside the 30-slot chart window) gives points.length > 0 but an all-zero chart + "Total logins: 0". Deciding emptiness from summarize(series).total === 0 keeps the empty state and stats consistent.
Low (nit) AnalyticsPage.tsx:132 "Busiest day" hint shows a raw ISO date (2026-07-23); a friendlier Jul 23 via toLocaleDateString would read better on the tile.

Minor notes

  • Stale PR description: it says direct navigation without analytics.read "shows the in-page 403 message", but the code now redirect("/dashboard")s on forbidden (AnalyticsPage.tsx:81-83), which the test confirms. The behavior is fine and self-consistent — just worth updating the description so it doesn't mislead.
  • checkPermission runs on every dashboard mount (one extra round-trip per load). Acceptable for UI gating; noting only for awareness.
  • I did not run bun run typecheck / vitest in this review; CI covers those gates.

Overall: approve-worthy. @abdulrafey1's feedback is resolved; only my three earlier non-blocking suggestions remain, at the author's discretion.
· zaira/feat/analytics-dashboard-ui

@regisb regisb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm definitely not the right person to review this frontend PR but I approve of this.

Base automatically changed from zaira/feat/analytics-dashboard-s3 to main July 28, 2026 17:01
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from 863ac4b to d365895 Compare July 28, 2026 17:20

@hamza-56 hamza-56 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.

Left a few inline comments, mostly structure/DRY stuff on the frontend. Backend side looks clean to me.

Comment thread frontend/app/dashboard/layout.tsx
Comment thread frontend/app/dashboard/layout.tsx
Comment thread frontend/app/dashboard/layout.tsx
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
Comment thread frontend/lib/analytics/client.ts Outdated
Comment thread frontend/lib/permissions/client.ts Outdated
Comment thread frontend/components/AppSidebar.tsx
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from d365895 to c29095f Compare July 29, 2026 07:32

@abdulrafey1 abdulrafey1 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.

I went through the PR with the help of claude, found some small issues, nothing major.
I think we all should perform claude-assisted in-depth code reviews for frontend so all of us can get a better idea of what are the common mistakes in the frontend code made by AI and how can we fix them

Comment thread frontend/app/dashboard/analytics/AnalyticsPage.test.tsx
Comment thread frontend/app/dashboard/layout.tsx
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
Comment thread frontend/components/AppSidebar.tsx
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx Outdated
Comment thread frontend/app/dashboard/analytics/AnalyticsPage.tsx
Comment thread frontend/components/ui/BarChart.tsx
Comment thread frontend/components/ui/BarChart.tsx Outdated
Comment thread frontend/app/dashboard/layout.tsx
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from c29095f to e44d63f Compare July 31, 2026 06:40
@zaira-bibi
zaira-bibi requested a review from abdulrafey1 July 31, 2026 06:53
Milestone 1's last leg: login activity is visible in-app. A client component
fetches GET /api/v1/analytics/login-activity once, zero-fills the sparse
(zero-day-omitting) response into a continuous 30-day series, and renders it as
a hand-rolled SVG bar chart plus two stat tiles (total logins, busiest day) —
derived from the same windowed series so the headline numbers match the bars.

The sidebar's Analytics nav entry is gated by asking the backend
GET /api/v1/permissions/can?permission=analytics.read (via a new lib/permissions
client), threaded through the dashboard layout as a boolean that fails closed —
there is no admin bypass. On a 403 (defense in depth for direct navigation),
AnalyticsPage redirects to /dashboard rather than showing a message, so it
discloses nothing about the gated page.

No new dependencies: BarChart and StatCard are added to the in-repo shadcn-style
components/ui set (themed SVG, dark-mode-native).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zaira-bibi
zaira-bibi force-pushed the zaira/feat/analytics-dashboard-ui branch from e44d63f to 36b5be2 Compare July 31, 2026 16:46
@zaira-bibi
zaira-bibi merged commit 9eeba0a into main Jul 31, 2026
8 of 9 checks passed
@zaira-bibi
zaira-bibi deleted the zaira/feat/analytics-dashboard-ui branch July 31, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analytics are not visible anywhere in the product UI

4 participants