From 10895977d4d3350abdfd6319b17e240f1eef9943 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 16:27:02 -0700 Subject: [PATCH] chore(admin-web): clear dashboard warning sweep What changed: - Removed unused admin dashboard imports and props. - Wrapped dashboard loaders in stable callbacks for hook dependency correctness. - Rendered experiment list load errors and migrated the security QR image to next/image. Warning impact: - @bytelyst/admin-web scoped warnings: 16 -> 0. - Workspace warning total: 173 -> 157. Verification: - pnpm --filter @bytelyst/admin-web exec eslint . --ext .ts,.tsx - pnpm --filter @bytelyst/admin-web typecheck - pnpm --filter @bytelyst/admin-web test - pnpm --filter @bytelyst/admin-web build - pnpm lint --- .../app/(dashboard)/experiments/[id]/page.tsx | 81 +++++++++------- .../src/app/(dashboard)/experiments/page.tsx | 8 ++ .../(dashboard)/predictive/at-risk/page.tsx | 96 ++++++++++--------- .../(dashboard)/predictive/campaigns/page.tsx | 89 ++++++++++------- .../(dashboard)/settings/security/page.tsx | 6 +- .../src/app/(dashboard)/surveys/[id]/page.tsx | 73 +++++++------- .../src/app/(dashboard)/surveys/page.tsx | 61 +++++------- 7 files changed, 237 insertions(+), 177 deletions(-) diff --git a/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx b/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx index c65f0cc0..07ef2680 100644 --- a/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/experiments/[id]/page.tsx @@ -5,7 +5,7 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import { @@ -20,7 +20,6 @@ import { TrendingUp, Clock, Sparkles, - Download, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -43,13 +42,7 @@ export default function ExperimentDetailPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - fetchExperimentData(); - const interval = setInterval(fetchExperimentData, 30000); // Auto-refresh every 30s - return () => clearInterval(interval); - }, [experimentId]); - - async function fetchExperimentData() { + const fetchExperimentData = useCallback(async () => { try { const [expResponse, resultsResponse] = await Promise.all([ fetch(`/api/experiments/${experimentId}`), @@ -67,7 +60,13 @@ export default function ExperimentDetailPage() { } finally { setLoading(false); } - } + }, [experimentId]); + + useEffect(() => { + fetchExperimentData(); + const interval = setInterval(fetchExperimentData, 30000); // Auto-refresh every 30s + return () => clearInterval(interval); + }, [fetchExperimentData]); async function updateStatus(status: string) { try { @@ -166,8 +165,8 @@ export default function ExperimentDetailPage() { Winner Found! - Variant has {((results.winnerProbability || 0) * 100).toFixed(1)}% probability of being best. - Recommended action: {results.statisticalSummary.recommendedAction}. + Variant has {((results.winnerProbability || 0) * 100).toFixed(1)}% probability of being + best. Recommended action: {results.statisticalSummary.recommendedAction}. )} @@ -211,7 +210,6 @@ export default function ExperimentDetailPage() { key={variant.id} variant={variant} isControl={variant.isControl} - experiment={experiment} result={results?.variantResults.find(vr => vr.variantId === variant.id)} /> ))} @@ -243,9 +241,7 @@ export default function ExperimentDetailPage() {
-
- Recommended Action -
+
Recommended Action
{results.statisticalSummary.recommendedAction}
@@ -256,7 +252,10 @@ export default function ExperimentDetailPage() {

Variant Comparison

{results.variantResults.map(vr => ( -
+
{vr.variantName}
@@ -267,8 +266,11 @@ export default function ExperimentDetailPage() {
-
0 ? 'text-green-600' : 'text-red-600'}`}> - {vr.expectedLiftPercent > 0 ? '+' : ''}{vr.expectedLiftPercent.toFixed(1)}% +
0 ? 'text-green-600' : 'text-red-600'}`} + > + {vr.expectedLiftPercent > 0 ? '+' : ''} + {vr.expectedLiftPercent.toFixed(1)}%
@@ -327,7 +329,9 @@ export default function ExperimentDetailPage() {
-

{experiment.allocationStrategy}

+

+ {experiment.allocationStrategy} +

@@ -339,15 +343,21 @@ export default function ExperimentDetailPage() {
-

{experiment.guardrails?.minSampleSizePerVariant} per variant

+

+ {experiment.guardrails?.minSampleSizePerVariant} per variant +

-

{experiment.guardrails?.maxDurationDays} days

+

+ {experiment.guardrails?.maxDurationDays} days +

-

{experiment.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'}

+

+ {experiment.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'} +

@@ -388,13 +398,15 @@ function StatCard({ function VariantCard({ variant, isControl, - experiment, result, }: { variant: VariantDoc; isControl: boolean; - experiment: ExperimentDoc; - result?: { probabilityBeatsControl: number; expectedLiftPercent: number; credibleInterval: { lower: number; mean: number; upper: number } }; + result?: { + probabilityBeatsControl: number; + expectedLiftPercent: number; + credibleInterval: { lower: number; mean: number; upper: number }; + }; }) { const conversionRate = variant.stats?.conversionRate || 0; const participants = variant.stats?.participants || 0; @@ -412,12 +424,13 @@ function VariantCard({ Control )} - {variant.bayesianResults?.probabilityBeatsControl && variant.bayesianResults.probabilityBeatsControl > 0.95 && ( - - - Winner - - )} + {variant.bayesianResults?.probabilityBeatsControl && + variant.bayesianResults.probabilityBeatsControl > 0.95 && ( + + + Winner + + )}

{variant.description}

@@ -475,5 +488,7 @@ function getStatusBadge(status: string) { stopped: 'bg-red-500', completed: 'bg-blue-500', }; - return {status}; + return ( + {status} + ); } diff --git a/dashboards/admin-web/src/app/(dashboard)/experiments/page.tsx b/dashboards/admin-web/src/app/(dashboard)/experiments/page.tsx index e1df9ffa..f39cb869 100644 --- a/dashboards/admin-web/src/app/(dashboard)/experiments/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/experiments/page.tsx @@ -25,6 +25,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; import type { ExperimentDoc } from '@/lib/experiments-types'; const statusConfig: Record = { @@ -140,6 +141,13 @@ export default function ExperimentsPage() {
+ {error && ( + + + {error} + + )} + {/* Tabs */} diff --git a/dashboards/admin-web/src/app/(dashboard)/predictive/at-risk/page.tsx b/dashboards/admin-web/src/app/(dashboard)/predictive/at-risk/page.tsx index d32b68dd..e4a81180 100644 --- a/dashboards/admin-web/src/app/(dashboard)/predictive/at-risk/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/predictive/at-risk/page.tsx @@ -1,13 +1,19 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Table, TableBody, @@ -23,16 +29,7 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import { - AlertTriangle, - Users, - RefreshCw, - Search, - ArrowRight, - TrendingUp, - Mail, - Activity, -} from 'lucide-react'; +import { AlertTriangle, Users, RefreshCw, Search, TrendingUp, Mail, Activity } from 'lucide-react'; import Link from 'next/link'; import { getAtRiskUsers, @@ -42,22 +39,25 @@ import { type UserRiskProfile, } from '@/lib/predictive-client'; -const riskSegmentConfig: Record = { - critical: { color: 'bg-red-500', label: 'Critical Risk', icon: }, +const riskSegmentConfig: Record< + RiskSegment, + { color: string; label: string; icon: React.ReactNode } +> = { + critical: { + color: 'bg-red-500', + label: 'Critical Risk', + icon: , + }, high: { color: 'bg-orange-500', label: 'High Risk', icon: }, medium: { color: 'bg-yellow-500', label: 'Medium Risk', icon: }, low: { color: 'bg-green-500', label: 'Low Risk', icon: }, }; -function UserDetailDialog({ userId, productId }: { userId: string; productId: string }) { +function UserDetailDialog({ userId }: { userId: string }) { const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - loadProfile(); - }, [userId, productId]); - - async function loadProfile() { + const loadProfile = useCallback(async () => { try { const data = await getUserRiskProfile(userId); setProfile(data); @@ -66,7 +66,11 @@ function UserDetailDialog({ userId, productId }: { userId: string; productId: st } finally { setLoading(false); } - } + }, [userId]); + + useEffect(() => { + loadProfile(); + }, [loadProfile]); if (loading) { return ; @@ -98,7 +102,9 @@ function UserDetailDialog({ userId, productId }: { userId: string; productId: st {profile.explanation.topRiskFactors.slice(0, 5).map((factor, idx) => (
{factor.feature} - + {(factor.contribution * 100).toFixed(1)}%
@@ -129,11 +135,14 @@ function UserDetailDialog({ userId, productId }: { userId: string; productId: st {new Date(intervention.timestamp).toLocaleDateString()} {intervention.outcome && ( - + {intervention.outcome} )} @@ -157,11 +166,7 @@ export default function AtRiskUsersPage() { const [offset, setOffset] = useState(0); const limit = 20; - useEffect(() => { - loadUsers(); - }, [selectedSegment, selectedProduct, offset]); - - async function loadUsers() { + const loadUsers = useCallback(async () => { try { setLoading(true); setError(null); @@ -178,17 +183,21 @@ export default function AtRiskUsersPage() { } finally { setLoading(false); } - } + }, [offset, selectedProduct, selectedSegment]); - const filteredUsers = users.filter((user) => + useEffect(() => { + loadUsers(); + }, [loadUsers]); + + const filteredUsers = users.filter(user => searchQuery ? user.userId.toLowerCase().includes(searchQuery.toLowerCase()) : true ); const segmentCounts = { - critical: users.filter((u) => u.riskSegment === 'critical').length, - high: users.filter((u) => u.riskSegment === 'high').length, - medium: users.filter((u) => u.riskSegment === 'medium').length, - low: users.filter((u) => u.riskSegment === 'low').length, + critical: users.filter(u => u.riskSegment === 'critical').length, + high: users.filter(u => u.riskSegment === 'high').length, + medium: users.filter(u => u.riskSegment === 'medium').length, + low: users.filter(u => u.riskSegment === 'low').length, }; return ( @@ -256,11 +265,11 @@ export default function AtRiskUsersPage() { setSearchQuery(e.target.value)} + onChange={e => setSearchQuery(e.target.value)} className="pl-9" />
- setSelectedSegment(v as RiskSegment)}> @@ -337,7 +346,7 @@ export default function AtRiskUsersPage() { ) : ( - filteredUsers.map((user) => ( + filteredUsers.map(user => ( {user.userId} {user.productId} @@ -361,7 +370,7 @@ export default function AtRiskUsersPage() { User Risk Profile - + @@ -374,7 +383,8 @@ export default function AtRiskUsersPage() { {/* Pagination */}
- Showing {offset + 1}-{Math.min(offset + filteredUsers.length, total)} of {total} users + Showing {offset + 1}-{Math.min(offset + filteredUsers.length, total)} of {total}{' '} + users