Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 28 additions & 4 deletions app/app/api/scan/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { checkRateLimit } from "@/lib/rate-limit";
const SCAN_SERVICE_URL = process.env.SCAN_SERVICE_URL ?? "";
const SCAN_TIMEOUT_MS = 60_000;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

export async function POST(req: Request) {
const clientIp = getClientIp(req);
const rateCheck = checkRateLimit(clientIp, 5, 60_000);
Expand Down Expand Up @@ -76,18 +80,38 @@ export async function POST(req: Request) {
return NextResponse.json(err, { status: scanResponse.status });
}

const result = await scanResponse.json();
let result: unknown;
try {
result = await scanResponse.json();
} catch {
return NextResponse.json(
{ error: "Scan service returned an invalid response" },
{ status: 502 },
);
}

if (
!isRecord(result) ||
typeof result.score !== "number" ||
typeof result.totalViolations !== "number"
) {
return NextResponse.json(
{ error: "Scan service returned an invalid response" },
{ status: 502 },
);
}

/* Persist to Supabase */
const db = createServerClient();
const violations = Array.isArray(result.violations) ? result.violations : [];
const { data: scan, error: insertError } = await db
.from("scans")
.insert({
url,
score: result.score,
total_violations: result.totalViolations,
violations: result.violations,
top_issues: result.violations?.slice(0, 5) ?? [],
violations,
top_issues: violations.slice(0, 5),
passed_rules: result.passedRules ?? 0,
total_rules: result.totalRules ?? 0,
})
Expand All @@ -106,6 +130,6 @@ export async function POST(req: Request) {
id: scan.id,
score: result.score,
totalViolations: result.totalViolations,
violations: result.violations,
violations,
});
}
30 changes: 21 additions & 9 deletions app/app/scan/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,25 @@ export default function ScanResultsPage({
const [errorMsg, setErrorMsg] = useState("");
const [revenueEstimate, setRevenueEstimate] = useState<RevenueEstimate | null>(null);
const [revenueLoading, setRevenueLoading] = useState(false);
const [revenueError, setRevenueError] = useState<string | null>(null);

const fetchScan = useCallback(async () => {
const res = await fetch(`/api/scans/${id}`);

if (!res.ok) {
setErrorMsg("Scan not found. It may still be processing or the link is invalid.");
try {
const res = await fetch(`/api/scans/${id}`);

if (!res.ok) {
setErrorMsg("Scan not found. It may still be processing or the link is invalid.");
setState("error");
return;
}

const data = (await res.json()) as ScanData;
setScan(data);
setState("ready");
} catch {
setErrorMsg("Could not reach the server. Please check your connection and try again.");
setState("error");
return;
}

const data = (await res.json()) as ScanData;
setScan(data);
setState("ready");
}, [id]);

useEffect(() => {
Expand All @@ -63,6 +69,7 @@ export default function ScanResultsPage({

async function handleRevenueSubmit(input: RevenueFormInput) {
setRevenueLoading(true);
setRevenueError(null);
try {
// Calculate client-side immediately for instant feedback
const estimate = calculateRevenueUplift({
Expand All @@ -79,7 +86,9 @@ export default function ScanResultsPage({
body: JSON.stringify({ scanId: id, ...input }),
}).catch((err) => console.warn("Could not persist revenue estimate:", err));
} catch (err) {
const message = err instanceof Error ? err.message : "Revenue calculation failed";
console.error("Revenue calculation failed:", err);
setRevenueError(message);
} finally {
setRevenueLoading(false);
}
Expand Down Expand Up @@ -173,6 +182,9 @@ export default function ScanResultsPage({
</div>

<Card padding="lg">
{revenueError ? (
<p role="alert" className="text-sm text-severity-critical">{revenueError}</p>
) : null}
{revenueEstimate ? (
<div className="space-y-8">
<RevenueResult estimate={revenueEstimate} />
Expand Down
22 changes: 14 additions & 8 deletions app/lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ let _browser: SupabaseClient | null = null;

export function getSupabase(): SupabaseClient {
if (!_browser) {
_browser = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

if (!url) throw new Error("NEXT_PUBLIC_SUPABASE_URL is not set");
if (!key) throw new Error("NEXT_PUBLIC_SUPABASE_ANON_KEY is not set");

_browser = createClient(url, key);
}
return _browser;
}
Expand All @@ -21,8 +24,11 @@ export { getSupabase as supabase };

/** Server-side Supabase client (service role, bypasses RLS) */
export function createServerClient(): SupabaseClient {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!url) throw new Error("NEXT_PUBLIC_SUPABASE_URL is not set");
if (!key) throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set");

return createClient(url, key);
}
4 changes: 3 additions & 1 deletion scan-service/src/notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export async function notifyFailure(reportId: string, error: string): Promise<vo
to: ALERT_EMAIL,
subject: `Report ${reportId.slice(0, 8)} failed`,
text: `Report ID: ${reportId}\nError: ${error}\nTime: ${new Date().toISOString()}`,
}).catch(() => {}); // best-effort, never throw
}).catch((err: unknown) => {
console.error('Failed to send failure alert:', err instanceof Error ? err.message : err);
});
}
9 changes: 7 additions & 2 deletions scan-service/src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,17 @@ async function scanUrlInternal(url: string): Promise<ScanResult> {
const SCAN_TIMEOUT_MS = 45_000;

export async function scanUrl(url: string): Promise<ScanResult> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
setTimeout(
timer = setTimeout(
() => reject(new Error(`Scan timed out after ${SCAN_TIMEOUT_MS / 1000}s`)),
SCAN_TIMEOUT_MS,
);
});

return Promise.race([scanUrlInternal(url), timeout]);
try {
return await Promise.race([scanUrlInternal(url), timeout]);
} finally {
if (timer) clearTimeout(timer);
}
}
4 changes: 2 additions & 2 deletions scan-service/src/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export async function captureAnnotatedScreenshot(
selector,
boundingBox: box,
});
} catch {
// Element might not be queryable, skip
} catch (err) {
console.warn(`Could not query element "${selector}":`, err instanceof Error ? err.message : err);
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions scan-service/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,15 @@ app.post('/api/generate-report', async (request, reply) => {
const message = error instanceof Error ? error.message : 'Report generation failed';
app.log.error({ reportId, error: message }, 'generate-report failed');

// Mark report as failed — do not swallow the DB error here
await db.from('reports').update({ status: 'failed', error_message: message }).eq('id', reportId);
// Mark report as failed
const { error: failError } = await db
.from('reports')
.update({ status: 'failed', error_message: message })
.eq('id', reportId);

if (failError) {
app.log.error({ reportId, dbError: failError.message }, 'failed to mark report as failed');
}

await notifyFailure(reportId, message);

Expand Down
8 changes: 4 additions & 4 deletions scan-service/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ export async function translateViolations(
if (validateTranslation(parsed)) {
return parsed.issues;
}
} catch {
// fall through to retry
} catch (error) {
console.error('LLM translation attempt 1 failed:', error instanceof Error ? error.message : error);
}

// Retry with explicit system instruction
Expand All @@ -172,8 +172,8 @@ export async function translateViolations(
if (validateTranslation(retryParsed)) {
return retryParsed.issues;
}
} catch {
// fall through to fallback
} catch (error) {
console.error('LLM translation attempt 2 failed:', error instanceof Error ? error.message : error);
}

return buildFallback(violations);
Expand Down
Loading