Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a273d19
feat(dashboard): add computeTimeSaved with honest + fallback formula
Gradata Apr 14, 2026
9101d5f
feat(dashboard): add computeWoWDelta with sample-size floor
Gradata Apr 14, 2026
1ff1ab8
feat(dashboard): add computeRuleStreak with graduated_at fallback
Gradata Apr 14, 2026
68d9877
feat(dashboard): extend Lesson type with recurrence_blocked, last_rec…
Gradata Apr 14, 2026
b8a45ef
feat(dashboard): extend KpiMetrics with timeSavedMinutes + WoW deltas
Gradata Apr 14, 2026
de4082a
feat(dashboard): KpiStrip 5-card layout with Est. Time Saved + WoW de…
Gradata Apr 14, 2026
ecfcc84
refactor(dashboard): KpiStrip test-id targeting + remove dead delta f…
Gradata Apr 15, 2026
2db464a
feat(dashboard): ActiveRulesPanel glyphs + streak suffix + see-all link
Gradata Apr 15, 2026
f90f1a3
feat(dashboard): ActivityFeed outcome labels + demote meta-rule events
Gradata Apr 15, 2026
e57be55
feat(dashboard): graduation markers on CorrectionDecayCurve
Gradata Apr 15, 2026
271e75c
feat(dashboard): CategoriesChart classifier-health gate (70% threshold)
Gradata Apr 15, 2026
2433dc4
feat(dashboard): add /proof route with ABProofPanel + MethodologyLink
Gradata Apr 15, 2026
fb5b2aa
feat(dashboard): add Proof nav entry
Gradata Apr 15, 2026
ff96d53
refactor(dashboard): remove MetaRulesGrid/ABProofPanel/MethodologyLin…
Gradata Apr 15, 2026
bd75cd4
feat(dashboard): operator bypass + demo mode + dedupe setup CTAs
Gradata Apr 15, 2026
2c3dea2
fix(dashboard): CR round-1 + promote Preview CTA
Gradata Apr 15, 2026
4a1319b
feat(dashboard): marketify pass — plain-language labels
Gradata Apr 15, 2026
2df35cc
fix(dashboard): CR round-3 — demo activity, recurrence ordering, cate…
Gradata Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cloud/dashboard/app/(dashboard)/corrections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { useApi } from '@/hooks/useApi'
import type { Brain, Correction, PaginatedResponse } from '@/types/api'
import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
import { EmptyState } from '@/components/shared/EmptyState'
import Link from 'next/link'
import { Button } from '@/components/ui/button'

const SEVERITY_STYLE: Record<Correction['severity'], string> = {
trivial: 'bg-white/[0.04] text-[var(--color-body)]',
Expand Down Expand Up @@ -36,8 +34,7 @@ export default function CorrectionsPage() {
if (!primaryId) return (
<EmptyState
title="No brain yet"
description="Install the SDK and log your first correction to see it here."
action={<Link href="/setup"><Button>Get started →</Button></Link>}
description="Install the SDK and log your first correction to see it here. See Setup in the left nav for install instructions."
/>
)

Expand Down
79 changes: 48 additions & 31 deletions cloud/dashboard/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
import { EmptyState } from '@/components/shared/EmptyState'
import { Button } from '@/components/ui/button'
import { computeKpis, computeGraduationCounts } from '@/lib/analytics-client'
import {
demoAnalytics,
demoCorrections,
demoLessons,
demoActivityEvents,
} from '@/lib/fixtures/demo-dashboard'
import { KpiStrip } from '@/components/brain/KpiStrip'
import { GraduationProgressBar } from '@/components/brain/GraduationProgressBar'
import { CorrectionDecayCurve } from '@/components/brain/CorrectionDecayCurve'
import { ActiveRulesPanel } from '@/components/brain/ActiveRulesPanel'
import { CategoriesChart } from '@/components/brain/CategoriesChart'
import { MetaRulesGrid } from '@/components/brain/MetaRulesGrid'
import { ActivityFeed } from '@/components/brain/ActivityFeed'
import { PrivacyPosturePanel } from '@/components/brain/PrivacyPosturePanel'
import { ABProofPanel } from '@/components/brain/ABProofPanel'
import { MethodologyLink } from '@/components/brain/MethodologyLink'

export default function DashboardPage() {
const [range, setRange] = useState<'7d' | '30d' | '90d'>('30d')
const [demoMode, setDemoMode] = useState(false)

const { data: brains, loading: loadingBrains } = useApi<Brain[]>('/brains')
const primaryBrainId = brains?.[0]?.id ?? null
Expand All @@ -36,33 +39,42 @@ export default function DashboardPage() {
)

const corrections = useMemo<Correction[]>(() => {
if (demoMode) return demoCorrections
if (!correctionsResp) return []
return Array.isArray(correctionsResp) ? correctionsResp : correctionsResp.data
}, [correctionsResp])
}, [correctionsResp, demoMode])

const lessons = useMemo<Lesson[]>(() => {
if (demoMode) return demoLessons
if (!lessonsResp) return []
return Array.isArray(lessonsResp) ? lessonsResp : lessonsResp.data
}, [lessonsResp])
}, [lessonsResp, demoMode])

const effectiveAnalytics = demoMode ? demoAnalytics : analytics

const kpis = useMemo(
() => (analytics ? computeKpis(analytics, corrections, lessons) : null),
[analytics, corrections, lessons],
() => (effectiveAnalytics ? computeKpis(effectiveAnalytics, corrections, lessons) : null),
[effectiveAnalytics, corrections, lessons],
)
const gradCounts = useMemo(() => computeGraduationCounts(lessons), [lessons])

if (loadingBrains) return <LoadingSpinner className="py-20" />
if (loadingBrains && !demoMode) return <LoadingSpinner className="py-20" />

if (!primaryBrainId) {
if (!primaryBrainId && !demoMode) {
return (
<div className="py-12">
<EmptyState
title="AI that learns the corrections you keep making"
description="Install the Gradata SDK and run your first session. Your brain stays local — the dashboard is a lens over it."
action={
<Link href="/setup">
<Button>Start setup →</Button>
</Link>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button onClick={() => setDemoMode(true)}>
Preview with sample data →
</Button>
<Link href="/setup">
<Button variant="outline">Install the SDK</Button>
</Link>
Comment on lines +74 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

BUTTON_FILE="$(fd -i '^button\.tsx$' | head -n1)"
sed -n '1,220p' "$BUTTON_FILE"
rg -n -C2 'asChild|Slot|function Button|const Button|buttonVariants' "$BUTTON_FILE"

Repository: Gradata/gradata

Length of output: 4335


🏁 Script executed:

# Search for Button usage patterns in the dashboard
rg -n 'asChild' cloud/dashboard --type=tsx --type=ts | head -20

Repository: Gradata/gradata

Length of output: 86


🏁 Script executed:

# Check if Button is used with Link elsewhere in the codebase
rg -n '<Button' cloud/dashboard --type=tsx -A2 -B2 | grep -E '(Link|Button|asChild)' | head -40

Repository: Gradata/gradata

Length of output: 86


🏁 Script executed:

# Search for base-ui Button documentation or imports
rg -n '@base-ui/react' cloud/dashboard --type=tsx --type=ts | head -10

Repository: Gradata/gradata

Length of output: 86


🏁 Script executed:

# Search for asChild usage patterns (no type flag)
rg 'asChild' cloud/dashboard | head -20

Repository: Gradata/gradata

Length of output: 167


🏁 Script executed:

# Check Button and Link combinations in dashboard
rg -A3 -B1 '<Button' cloud/dashboard | grep -E '(Link|Button|href)' | head -30

Repository: Gradata/gradata

Length of output: 2879


🏁 Script executed:

# Look for any imports related to asChild or composition
rg 'asChild|Slot|polymorphic|as=' cloud/dashboard | head -20

Repository: Gradata/gradata

Length of output: 167


🏁 Script executed:

# Find package.json to check `@base-ui` version
find cloud/dashboard -name 'package.json' -exec cat {} \; | grep -A5 -B5 '@base-ui'

Repository: Gradata/gradata

Length of output: 354


🏁 Script executed:

# Check if ButtonPrimitive.Props type hints at asChild support
rg 'ButtonPrimitive\.Props|@base-ui/react/button' cloud/dashboard --context=10

Repository: Gradata/gradata

Length of output: 2923


🏁 Script executed:

# Look at how base-ui is imported and used throughout
find cloud/dashboard -type f -name '*.tsx' -o -name '*.ts' | xargs grep -l '@base-ui' | head -10

Repository: Gradata/gradata

Length of output: 515


🌐 Web query:

@base-ui/react Button component asChild polymorphic composition API documentation

💡 Result:

The @base-ui/react Button component does not use an asChild prop for polymorphic composition. Instead, it uses a render prop combined with nativeButton for rendering as different elements while maintaining button semantics and accessibility. Key API details from official documentation: - render: ReactElement | ((props: HTMLProps, state: Button.State) => ReactElement) — Allows replacing the component's HTML element with a different tag or composing with another component. Example for custom tag: <Button render={

} nativeButton={false}> Button that can contain complex children - nativeButton: boolean (default: true) — Set to false when rendering non-button elements via render to adjust default props like omitting type="button". - focusableWhenDisabled: boolean (default: false) — Makes the button focusable even when disabled (useful for loading states). Note: There is an open GitHub issue (#3983, dated 2026-02-05) proposing to deprecate render in favor of asChild with children, but official docs as of 2026-04-15 still document render. Base UI intentionally uses render over Radix-style asChild/Slot, powered by the useRender hook for better type safety and explicitness. For full composition guide: https://base-ui.com/react/handbook/composition

Citations:


🏁 Script executed:

# Check if other base-ui components use render prop or similar pattern
rg 'render=' cloud/dashboard/src/components/ui/ --context=3

Repository: Gradata/gradata

Length of output: 2269


🏁 Script executed:

# Verify the exact implementation by checking imports from base-ui
rg 'from "@base-ui/react' cloud/dashboard/src/components/ui/ --context=2

Repository: Gradata/gradata

Length of output: 3404


Use the render prop instead of asChild to avoid nesting interactive elements.

The Button component wraps @base-ui/react/button, which does not support asChild. Instead, @base-ui uses a render prop for polymorphic composition. The current markup creates invalid nested interactive content (<a><button/></a>). Use the render prop with nativeButton={false} to fix this:

Suggested fix
-              <Link href="/setup">
-                <Button variant="outline">Install the SDK</Button>
-              </Link>
+              <Button 
+                variant="outline" 
+                render={<Link href="/setup">Install the SDK</Link>}
+                nativeButton={false}
+              />

Note: This pattern appears in multiple files (forgot-password, signup) and should be addressed consistently across the dashboard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cloud/dashboard/app/`(dashboard)/dashboard/page.tsx around lines 74 - 76,
Replace the invalid nested interactive markup where Link wraps Button by using
Button's render prop instead of asChild; locate the Link and Button usage (e.g.,
Link + Button in dashboard/page.tsx and similar occurrences in forgot-password
and signup) and change Button to use render with nativeButton={false} so it
renders an anchor (or forwards props) instead of rendering a native button
inside the Link, ensuring you pass Link's href and children via the render
callback.

</div>
}
/>
<pre className="mx-auto mt-6 w-fit rounded-[0.5rem] border border-[var(--color-border)] bg-[rgba(21,29,48,0.6)] px-5 py-3 font-mono text-[13px] text-[var(--color-accent-blue)]">
Expand All @@ -77,12 +89,28 @@ export default function DashboardPage() {

return (
<>
{/* Demo banner */}
{demoMode && (
<div className="mb-6 flex items-center justify-between gap-3 rounded-[0.5rem] border border-[rgba(234,179,8,0.3)] bg-[rgba(234,179,8,0.08)] px-4 py-2.5">
<span className="text-[12px] text-[var(--color-warning)]">
<strong>Demo mode</strong> — showing sample data. Install the SDK to see your own brain.
</span>
<button
type="button"
onClick={() => setDemoMode(false)}
className="text-[12px] text-[var(--color-body)] hover:text-[var(--color-text)]"
>
Exit demo
</button>
</div>
)}

{/* Page header + time range pills */}
<header className="mb-7 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-[22px]">Overview</h1>
<p className="mt-1 text-[13px] text-[var(--color-body)]">
Your brain&apos;s learning progress
What your AI learned from you
</p>
</div>
<div className="flex flex-wrap gap-1.5">
Expand All @@ -107,33 +135,22 @@ export default function DashboardPage() {
{kpis && <KpiStrip metrics={kpis} />}

{/* Hero: correction decay curve */}
<CorrectionDecayCurve corrections={corrections} range={range} />
<CorrectionDecayCurve corrections={corrections} lessons={lessons} range={range} />

{/* Graduation pipeline (3-tier, sim-validated as the moat) */}
{/* Graduation pipeline (3-tier, sim-validated as the moat) — thin strip */}
<div className="mb-4">
<GraduationProgressBar counts={gradCounts} />
</div>

{/* Rules + Categories */}
<div className="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
<ActiveRulesPanel lessons={lessons} />
{analytics && <CategoriesChart analytics={analytics} />}
</div>

{/* Meta rules + Activity */}
<div className="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
<MetaRulesGrid />
<ActivityFeed />
{effectiveAnalytics && <CategoriesChart analytics={effectiveAnalytics} />}
</div>

{/* Trust surface: privacy + A/B proof */}
<div className="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2">
<PrivacyPosturePanel footprintKb={kpis?.footprintKb ?? 0} />
<ABProofPanel />
</div>

<div className="text-center">
<MethodologyLink />
{/* Activity */}
<div className="mb-4">
<ActivityFeed events={demoMode ? demoActivityEvents : undefined} />
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</>
)
Expand Down
3 changes: 2 additions & 1 deletion cloud/dashboard/app/(dashboard)/meta-rules/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { MetaRulesGrid } from '@/components/brain/MetaRulesGrid'
import { PlanGate, type PlanTier } from '@/components/brain/PlanBadge'
import { isOperatorEmail } from '@/lib/operator'
import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
import { useApi } from '@/hooks/useApi'
import type { UserProfile } from '@/types/api'
Expand All @@ -21,7 +22,7 @@ export default function MetaRulesPage() {
</p>
</header>

<PlanGate current={currentPlan} requires="cloud" featureName="Meta rules">
<PlanGate current={currentPlan} requires="cloud" featureName="Meta rules" bypass={isOperatorEmail(profile?.email)}>
<MetaRulesGrid />
</PlanGate>
</>
Expand Down
5 changes: 1 addition & 4 deletions cloud/dashboard/app/(dashboard)/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { useApi } from '@/hooks/useApi'
import type { Brain, BrainAnalytics } from '@/types/api'
import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
import { EmptyState } from '@/components/shared/EmptyState'
import Link from 'next/link'
import { Button } from '@/components/ui/button'

export default function PrivacyPage() {
const { data: brains, loading: loadingBrains } = useApi<Brain[]>('/brains')
Expand All @@ -24,8 +22,7 @@ export default function PrivacyPage() {
if (!primaryId) return (
<EmptyState
title="No brain yet"
description="Install the SDK first — privacy controls appear per brain."
action={<Link href="/setup"><Button>Get started →</Button></Link>}
description="Install the SDK first — privacy controls appear per brain. See Setup in the left nav for install instructions."
/>
)

Expand Down
20 changes: 20 additions & 0 deletions cloud/dashboard/app/(dashboard)/proof/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import { ABProofPanel } from '@/components/brain/ABProofPanel'
import { MethodologyLink } from '@/components/brain/MethodologyLink'

export default function ProofPage() {
return (
<div className="py-8 space-y-6">
<div>
<h1 className="text-2xl font-bold">Proof</h1>
<p className="mt-1 text-[14px] text-[var(--color-body)]">
How we know your brain is actually learning: ablation data, methodology, and
independent replications.
</p>
</div>
<ABProofPanel />
<MethodologyLink />
</div>
)
}
5 changes: 1 addition & 4 deletions cloud/dashboard/app/(dashboard)/rules/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { useApi } from '@/hooks/useApi'
import type { Brain, Lesson, PaginatedResponse } from '@/types/api'
import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
import { EmptyState } from '@/components/shared/EmptyState'
import Link from 'next/link'
import { Button } from '@/components/ui/button'

const STATE_STYLE: Record<Lesson['state'], string> = {
INSTINCT: 'bg-[rgba(58,130,255,0.12)] text-[var(--color-accent-blue)]',
Expand Down Expand Up @@ -35,8 +33,7 @@ export default function RulesPage() {
if (!primaryId) return (
<EmptyState
title="No brain yet"
description="Install the SDK and graduate your first rule to see it here."
action={<Link href="/setup"><Button>Get started →</Button></Link>}
description="Install the SDK and graduate your first rule to see it here. See Setup in the left nav for install instructions."
/>
)

Expand Down
3 changes: 2 additions & 1 deletion cloud/dashboard/app/(dashboard)/self-healing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'
import { GlassCard } from '@/components/layout/GlassCard'
import { Button } from '@/components/ui/button'
import { PlanGate, type PlanTier } from '@/components/brain/PlanBadge'
import { isOperatorEmail } from '@/lib/operator'
import { useApi } from '@/hooks/useApi'
import type { Brain, UserProfile } from '@/types/api'
import api from '@/lib/api'
Expand Down Expand Up @@ -116,7 +117,7 @@ export default function SelfHealingPage() {
</p>
</header>

<PlanGate current={currentPlan} requires="cloud" featureName="Self-healing audit trail">
<PlanGate current={currentPlan} requires="cloud" featureName="Self-healing audit trail" bypass={isOperatorEmail(profile?.email)}>
<ul className="space-y-4">
{patches.map((p) => {
const isRolledBack = rolledBack[p.id]
Expand Down
3 changes: 2 additions & 1 deletion cloud/dashboard/app/(dashboard)/team/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,
} from '@/components/ui/dialog'
import { PlanGate, type PlanTier } from '@/components/brain/PlanBadge'
import { isOperatorEmail } from '@/lib/operator'
import { useApi } from '@/hooks/useApi'
import api from '@/lib/api'
import type { UserProfile } from '@/types/api'
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function TeamMembersPage() {
)}
</header>

<PlanGate current={currentPlan} requires="team" featureName="Team member management">
<PlanGate current={currentPlan} requires="team" featureName="Team member management" bypass={isOperatorEmail(profile?.email)}>
<GlassCard gradTop>
{loadingMembers ? (
<LoadingSpinner className="py-10" />
Expand Down
3 changes: 2 additions & 1 deletion cloud/dashboard/app/(dashboard)/team/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from 'next/link'
import { GlassCard } from '@/components/layout/GlassCard'
import { PlanGate, type PlanTier } from '@/components/brain/PlanBadge'
import { isOperatorEmail } from '@/lib/operator'
import { useApi } from '@/hooks/useApi'
import type { UserProfile } from '@/types/api'
import { LoadingSpinner } from '@/components/shared/LoadingSpinner'
Expand Down Expand Up @@ -64,7 +65,7 @@ export default function TeamOverviewPage() {
</Link>
</header>

<PlanGate current={currentPlan} requires="team" featureName="Team analytics">
<PlanGate current={currentPlan} requires="team" featureName="Team analytics" bypass={isOperatorEmail(profile?.email)}>
{loadingStats ? (
<LoadingSpinner className="py-12" />
) : (
Expand Down
Loading
Loading