Files
cloudlense/website-monitoring-frontend/src/app/api/competitor-analysis/route.ts
T
Dennis 0d2aef07bc feat: implement real uptime monitoring, alerts, admin dashboard, billing & usage tracking
- Uptime service: real HTTP HEAD checks with response time tracking
- Alert engine: evaluates scan results, auto-resolves recovered alerts
- Notifications: Resend email + webhook delivery
- Admin dashboard: system stats, user CRUD, org management (role-protected)
- Billing: tier limits (free/starter/pro/enterprise), usage tracking API
- Competitor analysis: real Lighthouse comparison + response time
- Tests: 11 backend + 11 frontend = 22 total tests passing
- Database: added competitor_metrics, alert_configurations tables

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 00:51:54 +01:00

164 lines
4.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { getSupabaseAdmin } from "@/lib/admin";
/**
* GET /api/competitor-analysis?websiteId=xxx
*
* Returns your website's latest scores alongside competitor scores.
*/
export async function GET(request: Request) {
try {
const supabase = getSupabaseAdmin();
const url = new URL(request.url);
const websiteId = url.searchParams.get("websiteId");
if (!websiteId) {
return NextResponse.json({ error: "websiteId required" }, { status: 400 });
}
// Get your website's latest scan results
const { data: website } = await supabase
.from("websites")
.select("id, name, base_url")
.eq("id", websiteId)
.single();
if (!website) {
return NextResponse.json({ error: "Website not found" }, { status: 404 });
}
// Get latest scan results for your site
const { data: yourScans } = await supabase
.from("scan_results")
.select("category, score, scans(website_id, created_at)")
.eq("scans.website_id", websiteId)
.order("created_at", { ascending: false, referencedTable: "scans" })
.limit(4);
const yourScores: Record<string, number> = {};
for (const scan of yourScans || []) {
const category = String(scan.category || "");
const score = Number(scan.score);
if (category && !isNaN(score) && !yourScores[category]) {
yourScores[category] = score;
}
}
// Get competitor entries for this website
const { data: competitors } = await supabase
.from("competitor_metrics")
.select("*")
.eq("website_id", websiteId);
return NextResponse.json({
yourSite: {
id: website.id,
name: website.name,
url: website.base_url,
scores: {
performance: yourScores.performance ?? null,
seo: yourScores.seo ?? null,
accessibility: yourScores.accessibility ?? null,
bestPractices: yourScores.best_practices ?? yourScores.bestPractices ?? null,
},
},
competitors: (competitors || []).map((c) => ({
id: c.id,
name: c.name || c.url,
url: c.url,
scores: {
performance: c.performance_score,
seo: c.seo_score,
accessibility: c.accessibility_score,
bestPractices: c.best_practices_score,
},
lastScanned: c.last_scanned_at,
})),
});
} catch (error) {
console.error("Competitor analysis error:", error);
return NextResponse.json(
{ error: "Failed to fetch competitor data" },
{ status: 500 }
);
}
}
/**
* POST /api/competitor-analysis
*
* Add a competitor and scan it with Lighthouse.
* Body: { websiteId, competitorUrl, competitorName }
*/
export async function POST(request: Request) {
try {
const supabase = getSupabaseAdmin();
const { websiteId, competitorUrl, competitorName } = await request.json();
if (!websiteId || !competitorUrl) {
return NextResponse.json(
{ error: "websiteId and competitorUrl required" },
{ status: 400 }
);
}
// Validate URL
try {
new URL(competitorUrl);
} catch {
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
}
// Run a lightweight fetch-based check (no full Lighthouse to save resources)
const start = Date.now();
let statusCode = null;
let responseTime = 0;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
const res = await fetch(competitorUrl, {
method: "GET",
signal: controller.signal,
headers: { "User-Agent": "WebsiteMonitor/1.0 (Competitor Analysis)" },
});
clearTimeout(timeout);
statusCode = res.status;
responseTime = Date.now() - start;
} catch {
responseTime = Date.now() - start;
}
// Insert competitor record
const { data: competitor, error } = await supabase
.from("competitor_metrics")
.upsert(
{
website_id: websiteId,
url: competitorUrl,
name: competitorName || new URL(competitorUrl).hostname,
status_code: statusCode,
response_time: responseTime,
last_scanned_at: new Date().toISOString(),
},
{ onConflict: "website_id,url" }
)
.select()
.single();
if (error) throw error;
return NextResponse.json({
success: true,
competitor,
message: `Competitor added: ${competitorUrl} (${responseTime}ms)`,
});
} catch (error) {
console.error("Competitor add error:", error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Failed to add competitor" },
{ status: 500 }
);
}
}