From 45b90590d9da3c61f1ba3ba39d722bfdcca38a25 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 9 May 2026 09:08:22 +0200 Subject: [PATCH 1/2] chore(web): apply react-doctor cleanup --- .agents/skills/react-doctor/SKILL.md | 32 ++++++++++++ .../claw/components/ChannelPairingStep.tsx | 27 +++++++--- .../KiloClawScheduledActionBanner.tsx | 6 ++- apps/web/src/app/(app)/install/page.tsx | 16 +++--- apps/web/src/app/(app)/learn/page.tsx | 16 +++--- .../admin/components/BlacklistedDomains.tsx | 24 ++++----- .../KiloclawInstances/KiloclawOrphansTab.tsx | 52 ++++++++++++------- .../src/app/auth/verify-magic-link/page.tsx | 14 ++--- apps/web/src/app/vscode-marketplace/page.tsx | 11 ++-- apps/web/src/components/AnimatedLogo.tsx | 26 +++++----- .../cloud-agent-next/EnvVarsDialog.tsx | 6 +-- .../cloud-agent-next/GlobToolCard.tsx | 16 +++--- .../cloud-agent-next/ListToolCard.tsx | 16 +++--- .../cloud-agent-next/SetupCommandsDialog.tsx | 6 +-- .../components/cloud-agent/EnvVarsDialog.tsx | 6 +-- .../cloud-agent/SetupCommandsDialog.tsx | 6 +-- .../OrganizationInvoicesCard.tsx | 30 +++++++---- .../organizations/custom-modes/ModeForm.tsx | 8 +-- .../subscription/SeatChangeModal.tsx | 25 +++++---- .../subscription/SubscriptionQuickActions.tsx | 18 +++---- .../organizations/subscription/utils.ts | 10 ++-- apps/web/src/hooks/useImageUpload.ts | 2 +- apps/web/src/lib/admin-utils.ts | 38 +++++++++----- apps/web/src/lib/blacklist-domains-config.ts | 8 +-- .../lib/organizations/organization-types.ts | 2 +- apps/web/src/lib/utils.ts | 33 ++++++++---- .../routers/admin/blacklist-domains-router.ts | 7 ++- 27 files changed, 284 insertions(+), 177 deletions(-) create mode 100644 .agents/skills/react-doctor/SKILL.md diff --git a/.agents/skills/react-doctor/SKILL.md b/.agents/skills/react-doctor/SKILL.md new file mode 100644 index 0000000000..22e73a50a5 --- /dev/null +++ b/.agents/skills/react-doctor/SKILL.md @@ -0,0 +1,32 @@ +--- +name: react-doctor +description: Use when finishing a feature, fixing a bug, before committing React code, or when the user wants to improve code quality or clean up a codebase. Checks for score regression. Covers lint, dead code, accessibility, bundle size, architecture diagnostics. +version: '1.0.0' +--- + +# React Doctor + +Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. + +## After making React code changes: + +Run `npx -y react-doctor@latest . --verbose --diff` and check the score did not regress. + +If the score dropped, fix the regressions before committing. + +## For general cleanup or code improvement: + +Run `npx -y react-doctor@latest . --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings. + +## Command + +```bash +npx -y react-doctor@latest . --verbose --diff +``` + +| Flag | Purpose | +| ----------- | --------------------------------------------- | +| `.` | Scan current directory | +| `--verbose` | Show affected files and line numbers per rule | +| `--diff` | Only scan changed files vs base branch | +| `--score` | Output only the numeric score | diff --git a/apps/web/src/app/(app)/claw/components/ChannelPairingStep.tsx b/apps/web/src/app/(app)/claw/components/ChannelPairingStep.tsx index a78aea4033..c07e0bb34f 100644 --- a/apps/web/src/app/(app)/claw/components/ChannelPairingStep.tsx +++ b/apps/web/src/app/(app)/claw/components/ChannelPairingStep.tsx @@ -46,16 +46,27 @@ export function ChannelPairingStep({ useEffect(() => { let cancelled = false; + let timeoutId: ReturnType | undefined; + + function waitForNextPoll() { + return new Promise(resolve => { + timeoutId = setTimeout(resolve, 1_000); + }); + } + async function poll() { while (!cancelled) { await refreshRef.current().catch(() => {}); if (cancelled) break; - await new Promise(r => setTimeout(r, 1_000)); + await waitForNextPoll(); } } void poll(); return () => { cancelled = true; + if (timeoutId) { + clearTimeout(timeoutId); + } }; }, []); @@ -143,7 +154,7 @@ export function ChannelPairingStepView({ >
- + Pairing request received @@ -175,9 +186,9 @@ export function ChannelPairingStepView({ disabled={isApproving} > {isApproving ? ( - + ) : ( - + )} Authorize this request @@ -187,7 +198,7 @@ export function ChannelPairingStepView({ className="text-muted-foreground/50 hover:text-muted-foreground mx-auto flex cursor-pointer items-center gap-1.5 text-sm transition-colors" onClick={onSkip} > - + Decline @@ -203,7 +214,7 @@ export function ChannelPairingStepView({ contentClassName="gap-8" >
-
+

- Waiting for you to message the bot... + Waiting for you to message the bot…

This page will update automatically.

@@ -247,7 +258,7 @@ export function ChannelPairingStepView({ className="text-muted-foreground/50 cursor-pointer hover:text-muted-foreground text-sm transition-colors my-6" onClick={onSkip} > - Skip — I'll pair later from Settings + Skip. I'll pair later from Settings
diff --git a/apps/web/src/app/(app)/claw/components/KiloClawScheduledActionBanner.tsx b/apps/web/src/app/(app)/claw/components/KiloClawScheduledActionBanner.tsx index cfd5c77b9e..f36c9b5306 100644 --- a/apps/web/src/app/(app)/claw/components/KiloClawScheduledActionBanner.tsx +++ b/apps/web/src/app/(app)/claw/components/KiloClawScheduledActionBanner.tsx @@ -17,6 +17,8 @@ import { CalendarClock } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import type { KiloClawScheduledActionStatusBlock } from '@/lib/kiloclaw/types'; +const timezoneNameFormatter = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }); + type Props = { scheduledAction: KiloClawScheduledActionStatusBlock | null; /** @@ -47,7 +49,7 @@ function formatScheduledAt(iso: string): string { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; const dateStr = d.toLocaleString(); - const tzPart = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }) + const tzPart = timezoneNameFormatter .formatToParts(d) .find(p => p.type === 'timeZoneName')?.value; return tzPart ? `${dateStr} ${tzPart}` : dateStr; @@ -80,7 +82,7 @@ export function KiloClawScheduledActionBanner({ scheduledAction, instanceName }: return ( - + {isVersionChange ? ( <> diff --git a/apps/web/src/app/(app)/install/page.tsx b/apps/web/src/app/(app)/install/page.tsx index 881e6dbffb..967b10aba9 100644 --- a/apps/web/src/app/(app)/install/page.tsx +++ b/apps/web/src/app/(app)/install/page.tsx @@ -44,10 +44,10 @@ export default function InstallPage() {
-
+
-
- +
+
IDE Extension @@ -57,7 +57,7 @@ export default function InstallPage() { {data?.updated_by_email && ( @@ -143,7 +143,7 @@ function StatsTab() { {stats.totalDomains} {stats.totalDomains === 1 ? 'domain' : 'domains'} - + {stats.totalBlockedUsers.toLocaleString()} blocked users
@@ -152,7 +152,7 @@ function StatsTab() {
{isLoading ? ( -
Loading stats...
+
Loading stats…
) : !stats || stats.domains.length === 0 ? (
No blacklisted domains configured @@ -219,7 +219,7 @@ function SuspiciousTab() { {domains.length} {domains.length === 1 ? 'domain' : 'domains'} - + {blacklistedCount} already blacklisted
@@ -254,12 +254,12 @@ function SuspiciousTab() { {domain.isBlacklisted ? ( - + Blacklisted ) : ( - + Not blacklisted )} diff --git a/apps/web/src/app/admin/components/KiloclawInstances/KiloclawOrphansTab.tsx b/apps/web/src/app/admin/components/KiloclawInstances/KiloclawOrphansTab.tsx index b7de2f41bf..b86a9a87dd 100644 --- a/apps/web/src/app/admin/components/KiloclawInstances/KiloclawOrphansTab.tsx +++ b/apps/web/src/app/admin/components/KiloclawInstances/KiloclawOrphansTab.tsx @@ -84,14 +84,14 @@ function TroubleshootingEventsDialog({
{isLoading && (
- - Loading events... + + Loading events…
)} {error && ( - + {error instanceof Error ? error.message : 'Failed to load Analytics Engine events'} @@ -148,10 +148,12 @@ export function KiloclawOrphansTab() { const trpc = useTRPC(); const queryClient = useQueryClient(); - const [createdAfterInput, setCreatedAfterInput] = useState( + const [createdAfterInput, setCreatedAfterInput] = useState(() => toDatetimeLocalInput(subDays(new Date(), 1)) ); - const [createdBeforeInput, setCreatedBeforeInput] = useState(toDatetimeLocalInput(new Date())); + const [createdBeforeInput, setCreatedBeforeInput] = useState(() => + toDatetimeLocalInput(new Date()) + ); const [scanResult, setScanResult] = useState<{ orphans: OrphanRow[]; scanned: number; @@ -187,11 +189,15 @@ export function KiloclawOrphansTab() { void queryClient.invalidateQueries({ queryKey: trpc.admin.kiloclawInstances.stats.queryKey(), }); - if (scanResult && destroyTarget) { - setScanResult({ - ...scanResult, - orphans: scanResult.orphans.filter(orphan => orphan.id !== destroyTarget.id), - }); + if (destroyTarget) { + setScanResult(prevScanResult => + prevScanResult + ? { + ...prevScanResult, + orphans: prevScanResult.orphans.filter(orphan => orphan.id !== destroyTarget.id), + } + : prevScanResult + ); } }, onError: err => { @@ -237,16 +243,22 @@ export function KiloclawOrphansTab() {
- + setCreatedAfterInput(e.target.value)} />
- + setCreatedBeforeInput(e.target.value)} @@ -255,12 +267,12 @@ export function KiloclawOrphansTab() {
@@ -440,8 +452,8 @@ export function KiloclawOrphansTab() { > {destroyOrphan.isPending ? ( <> - - Destroying... + + Destroying… ) : ( 'Destroy orphan' diff --git a/apps/web/src/app/auth/verify-magic-link/page.tsx b/apps/web/src/app/auth/verify-magic-link/page.tsx index aec7bd17cf..e902d673e6 100644 --- a/apps/web/src/app/auth/verify-magic-link/page.tsx +++ b/apps/web/src/app/auth/verify-magic-link/page.tsx @@ -30,7 +30,7 @@ function VerifyMagicLinkContent() { if (error) { return ( -
+
{error}
@@ -42,10 +42,10 @@ function VerifyMagicLinkContent() { } return ( -
+
-
-

Signing you in...

+
+

Signing you in…

); @@ -55,10 +55,10 @@ export default function VerifyMagicLinkPage() { return ( +
-
-

Loading...

+
+

Loading…

} diff --git a/apps/web/src/app/vscode-marketplace/page.tsx b/apps/web/src/app/vscode-marketplace/page.tsx index 3b32258fc7..9d795febde 100644 --- a/apps/web/src/app/vscode-marketplace/page.tsx +++ b/apps/web/src/app/vscode-marketplace/page.tsx @@ -14,17 +14,22 @@ export default function VSCodeMarketplaceRedirectPage() { }; // Posthog Bug workaround: posthog.capture() is not working without a timeout - setTimeout(() => { + const captureTimeout = setTimeout(() => { posthog?.capture('vscode_marketplace_redirect', { source: 'vscode-marketplace-page', }); }, 0); // Capture event immediately - setTimeout(() => { + const redirectTimeout = setTimeout(() => { performRedirect(); }, 500); + + return () => { + clearTimeout(captureTimeout); + clearTimeout(redirectTimeout); + }; }, [posthog]); // Optionally, render a fallback/loading state - return
Redirecting...
; + return
Redirecting…
; } diff --git a/apps/web/src/components/AnimatedLogo.tsx b/apps/web/src/components/AnimatedLogo.tsx index 74b66259bb..50266f2ad3 100644 --- a/apps/web/src/components/AnimatedLogo.tsx +++ b/apps/web/src/components/AnimatedLogo.tsx @@ -1,25 +1,23 @@ 'use client'; -import { useState, useRef, useEffect, useMemo } from 'react'; +import { useRef, useMemo } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { useSession } from 'next-auth/react'; export function AnimatedLogo() { - const [isHovered, setIsHovered] = useState(false); const videoRef = useRef(null); const { status } = useSession(); - useEffect(() => { - if (videoRef.current) { - if (isHovered) { - void videoRef.current.play(); - } else { - videoRef.current.pause(); - videoRef.current.currentTime = 0; // Reset to first frame - } - } - }, [isHovered]); + const playLogoAnimation = () => { + void videoRef.current?.play(); + }; + + const resetLogoAnimation = () => { + if (!videoRef.current) return; + videoRef.current.pause(); + videoRef.current.currentTime = 0; + }; const href = useMemo(() => { if (status === 'authenticated') { @@ -32,8 +30,8 @@ export function AnimatedLogo() { setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + onMouseEnter={playLogoAnimation} + onMouseLeave={resetLogoAnimation} >