feat: initialize monorepo with full dev team best practices
- Unified monorepo with backend (Express), frontend (Next.js), and devops - Backend: ESLint, Prettier, Jest tests (3 passing), health endpoint, .env.example - Frontend: Fixed build errors, fixed all lint errors (0 remaining), tests passing - DevOps: Docker Compose with PostgreSQL, backend, frontend + healthchecks - CI/CD: 3 GitHub Actions workflows (backend, frontend, docker integration) - DX: Husky pre-commit hooks with smart change detection - Docs: Root README with architecture, CONTRIBUTING.md, PR template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { scanScheduler } from "@/services/scanScheduler";
|
||||
import { lighthouseScanner } from "@/services/lighthouseScanner";
|
||||
import { logError } from "@/utils/errorUtils";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const mode = url.searchParams.get("mode") || "all"; // "scheduled", "change_detection", "all"
|
||||
const organizationId = url.searchParams.get("organizationId"); // Optional: limit to specific org
|
||||
|
||||
console.info(JSON.stringify({ level: 'info', event: 'scan_process_start', mode, timestamp: new Date().toISOString() }));
|
||||
|
||||
const results = {
|
||||
scheduledScans: 0,
|
||||
changeDetectionScans: 0,
|
||||
errors: [] as string[],
|
||||
startTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Process scheduled scans
|
||||
if (mode === "scheduled" || mode === "all") {
|
||||
try {
|
||||
console.info(JSON.stringify({ level: 'info', event: 'processing_scheduled_scans', timestamp: new Date().toISOString() }));
|
||||
await scanScheduler.processScheduledScans();
|
||||
|
||||
// Get count of processed scans
|
||||
const scheduledScans = await scanScheduler.getScheduledScans();
|
||||
results.scheduledScans = scheduledScans.length;
|
||||
|
||||
console.info(JSON.stringify({ level: 'info', event: 'scheduled_scans_processed', count: results.scheduledScans, timestamp: new Date().toISOString() }));
|
||||
} catch (error) {
|
||||
const errorMsg = `Error processing scheduled scans: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logError(errorMsg, error);
|
||||
results.errors.push(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Process change detection
|
||||
if (mode === "change_detection" || mode === "all") {
|
||||
try {
|
||||
console.info(JSON.stringify({ level: 'info', event: 'processing_change_detection', timestamp: new Date().toISOString() }));
|
||||
await scanScheduler.processChangeDetection();
|
||||
|
||||
// Note: Change detection count is harder to track since it's based on actual changes
|
||||
// We'll just indicate it was processed
|
||||
results.changeDetectionScans = -1; // -1 indicates processed but count unknown
|
||||
|
||||
console.info(JSON.stringify({ level: 'info', event: 'change_detection_processed', timestamp: new Date().toISOString() }));
|
||||
} catch (error) {
|
||||
const errorMsg = `Error processing change detection: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logError(errorMsg, error);
|
||||
results.errors.push(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Get overall statistics
|
||||
const stats = await getScanStatistics(organizationId ?? undefined);
|
||||
|
||||
const response = {
|
||||
success: results.errors.length === 0,
|
||||
message: `Automatic scan process completed - ${results.scheduledScans} scheduled scans, change detection processed`,
|
||||
results,
|
||||
statistics: stats,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.info(JSON.stringify({ level: 'info', event: 'scan_process_completed', success: response.success, timestamp: new Date().toISOString() }));
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
const errorMsg = `Critical error in automatic scan process: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logError(errorMsg, error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMsg,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scan statistics for monitoring
|
||||
*/
|
||||
async function getScanStatistics(organizationId?: string) {
|
||||
try {
|
||||
const { getSupabaseAdmin } = await import("@/lib/admin");
|
||||
const supabase = getSupabaseAdmin();
|
||||
|
||||
const now = new Date();
|
||||
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Build query
|
||||
let query = supabase
|
||||
.from('scans')
|
||||
.select('id, status, created_at, triggered_by');
|
||||
|
||||
if (organizationId) {
|
||||
const { data: websitesForOrg, error: orgErr } = await supabase
|
||||
.from('websites')
|
||||
.select('id')
|
||||
.eq('organization_id', organizationId);
|
||||
if (orgErr) {
|
||||
throw orgErr;
|
||||
}
|
||||
const websiteIds = (websitesForOrg || []).map((w: any) => w.id);
|
||||
if (websiteIds.length === 0) {
|
||||
return {
|
||||
today: { total: 0, byStatus: {}, byTrigger: {} },
|
||||
thisMonth: { total: 0 },
|
||||
last24Hours: { total: 0 },
|
||||
};
|
||||
}
|
||||
query = query.in('website_id', websiteIds as any[]);
|
||||
}
|
||||
|
||||
// Get today's scans
|
||||
const { data: todayScans } = await query
|
||||
.gte('created_at', startOfDay.toISOString());
|
||||
|
||||
// Get this month's scans
|
||||
const { data: monthScans } = await query
|
||||
.gte('created_at', startOfMonth.toISOString());
|
||||
|
||||
// Get scans by status
|
||||
const { data: statusCounts } = await query
|
||||
.select('status') as unknown as { data: Array<{ status: string }> };
|
||||
|
||||
const statusBreakdown = (statusCounts?.reduce((acc: Record<string, number>, scan: { status: string }) => {
|
||||
const key = String(scan.status || 'unknown');
|
||||
acc[key] = (acc[key] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>)) || {};
|
||||
|
||||
// Get scans by trigger type
|
||||
const { data: triggerCounts } = await query
|
||||
.select('triggered_by') as unknown as { data: Array<{ triggered_by: string | null }> };
|
||||
|
||||
const triggerBreakdown = (triggerCounts?.reduce((acc: Record<string, number>, scan: { triggered_by: string | null }) => {
|
||||
const trigger = String(scan.triggered_by || 'unknown');
|
||||
acc[trigger] = (acc[trigger] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>)) || {};
|
||||
|
||||
return {
|
||||
today: {
|
||||
total: todayScans?.length || 0,
|
||||
byStatus: statusBreakdown,
|
||||
byTrigger: triggerBreakdown,
|
||||
},
|
||||
thisMonth: {
|
||||
total: monthScans?.length || 0,
|
||||
},
|
||||
last24Hours: {
|
||||
total: todayScans?.length || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logError('Error getting scan statistics', error);
|
||||
return {
|
||||
today: { total: 0, byStatus: {}, byTrigger: {} },
|
||||
thisMonth: { total: 0 },
|
||||
last24Hours: { total: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual scan trigger endpoint
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { websiteId, pageId, deviceType = 'desktop', categories, priority = 'medium' } = body;
|
||||
|
||||
if (!websiteId || !pageId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Website ID and Page ID are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.info(JSON.stringify({ level: 'info', event: 'manual_scan_triggered', websiteId, pageId, timestamp: new Date().toISOString() }));
|
||||
|
||||
// Check subscription limits
|
||||
const { data: website } = await (await import("@/lib/admin")).getSupabaseAdmin()
|
||||
.from('websites')
|
||||
.select('organization_id')
|
||||
.eq('id', websiteId)
|
||||
.single();
|
||||
|
||||
if (!website) {
|
||||
return NextResponse.json(
|
||||
{ error: "Website not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const { canScan, limits, currentUsage } = await lighthouseScanner.checkSubscriptionLimits(
|
||||
String(website.organization_id)
|
||||
);
|
||||
|
||||
if (!canScan) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Subscription limit exceeded",
|
||||
limits,
|
||||
currentUsage,
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
// Perform the scan
|
||||
const scanConfig = {
|
||||
websiteId,
|
||||
pageId,
|
||||
deviceType: deviceType as 'desktop' | 'mobile',
|
||||
categories: categories || ['performance', 'accessibility', 'seo', 'best_practices'],
|
||||
priority: priority as 'low' | 'medium' | 'high',
|
||||
triggeredBy: 'manual' as const,
|
||||
};
|
||||
|
||||
const result = await lighthouseScanner.performScan(scanConfig);
|
||||
|
||||
if (result.success) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
scanId: result.scanId,
|
||||
message: "Scan completed successfully",
|
||||
metrics: result.metrics,
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: result.error,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `Error in manual scan: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logError(errorMsg, error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorMsg,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user