0d2aef07bc
- 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>
164 lines
4.7 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|