import { createClient } from '@supabase/supabase-js'; const STATUS_ENDPOINTS = { supabase: 'https://supabase.statuspage.io/api/v2/summary.json', vercel: 'https://www.vercel-status.com/api/v2/summary.json', }; const SUPABASE_LIMITS = { storageBytes: 1024 * 1024 * 1024, databaseBytes: 500 * 1024 * 1024, egressBytes: 5 * 1024 * 1024 * 1024, }; const VERCEL_LIMITS = { fastDataTransferBytes: 100 * 1024 * 1024 * 1024, edgeRequests: 1_000_000, functionInvocations: 1_000_000, activeCpuHours: 4, }; function json(res, status, body) { res.status(status).setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-store'); res.send(JSON.stringify(body)); } function toNumber(value) { if (value === undefined || value === null || value === '') return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function makeQuota(id, label, used, limit, unit, note, source = 'live') { return { id, label, used, limit, unit, note, source, }; } async function fetchJson(url) { const response = await fetch(url, { headers: { 'User-Agent': 'FourgePortal/1.0' }, }); if (!response.ok) { throw new Error(`Request failed (${response.status})`); } return response.json(); } async function fetchStatusSummary(url) { try { const summary = await fetchJson(url); return { ok: true, status: summary.status || null, updatedAt: summary.page?.updated_at || null, components: Array.isArray(summary.components) ? summary.components.map(component => ({ id: component.id, name: component.name, status: component.status, })) : [], }; } catch (error) { return { ok: false, status: { indicator: 'major', description: error.message || 'Status unavailable' }, updatedAt: null, components: [], }; } } async function requireTeam(authHeader) { const supabaseUrl = process.env.VITE_SUPABASE_URL || process.env.SUPABASE_URL; const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error('Supabase auth env is not configured on Vercel.'); } const callerClient = createClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: false, autoRefreshToken: false }, global: { headers: { Authorization: authHeader } }, }); const { data: userData, error: userError } = await callerClient.auth.getUser(); if (userError || !userData?.user) { return { ok: false, status: 401, message: 'Unauthorized' }; } const { data: profile, error: profileError } = await callerClient .from('profiles') .select('role') .eq('id', userData.user.id) .single(); if (profileError) { return { ok: false, status: 500, message: profileError.message }; } if (profile?.role !== 'team') { return { ok: false, status: 403, message: 'Forbidden' }; } return { ok: true, supabaseUrl }; } async function countRows(client, table) { const { count, error } = await client.from(table).select('id', { count: 'exact', head: true }); if (error) throw error; return count || 0; } async function listStorageFolder(storageClient, bucketId, folder = '') { const pageSize = 100; let offset = 0; const entries = []; while (true) { const { data, error } = await storageClient.from(bucketId).list(folder, { limit: pageSize, offset, sortBy: { column: 'name', order: 'asc' }, }); if (error) throw error; if (!data?.length) break; entries.push(...data); if (data.length < pageSize) break; offset += pageSize; } return entries; } function isStorageFile(entry) { return Boolean(entry?.metadata && typeof entry.metadata === 'object' && 'size' in entry.metadata); } async function getBucketUsage(storageClient, bucketId, folder = '') { let totalBytes = 0; let totalFiles = 0; const entries = await listStorageFolder(storageClient, bucketId, folder); for (const entry of entries) { if (isStorageFile(entry)) { totalFiles += 1; totalBytes += Number(entry.metadata.size || 0); continue; } const childFolder = folder ? `${folder}/${entry.name}` : entry.name; const childUsage = await getBucketUsage(storageClient, bucketId, childFolder); totalFiles += childUsage.totalFiles; totalBytes += childUsage.totalBytes; } return { totalBytes, totalFiles }; } async function getStorageUsage(admin) { const { data: buckets, error } = await admin.storage.listBuckets(); if (error) throw error; const bucketEntries = []; let totalBytes = 0; let totalFiles = 0; for (const bucket of buckets || []) { const bucketUsage = await getBucketUsage(admin.storage, bucket.id); totalBytes += bucketUsage.totalBytes; totalFiles += bucketUsage.totalFiles; bucketEntries.push({ bucketId: bucket.id, bytes: bucketUsage.totalBytes }); } bucketEntries.sort((a, b) => b.bytes - a.bytes); return { totalBytes, totalFiles, buckets: bucketEntries }; } async function getSupabaseUsageSnapshot(supabaseUrl, overridesResult) { const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const databaseBytesFallback = toNumber(process.env.SUPABASE_DB_SIZE_BYTES); const egressBytesFallback = toNumber(process.env.SUPABASE_EGRESS_BYTES); const overrides = overridesResult?.data || null; const egressBytes = toNumber(overrides?.supabase_egress_bytes) ?? egressBytesFallback; if (!serviceRoleKey) { return { configured: false, message: 'Set SUPABASE_SERVICE_ROLE_KEY on Vercel to show live Supabase usage.', quotas: [ makeQuota('storage', 'Storage', null, SUPABASE_LIMITS.storageBytes, 'bytes', 'Live storage usage is not configured.', 'unavailable'), makeQuota('database', 'Database size', databaseBytesFallback, SUPABASE_LIMITS.databaseBytes, 'bytes', databaseBytesFallback === null ? 'Run the latest migration or set SUPABASE_DB_SIZE_BYTES manually.' : 'Manual value from env.', databaseBytesFallback === null ? 'unavailable' : 'manual'), makeQuota('egress', 'Egress', egressBytes, SUPABASE_LIMITS.egressBytes, 'bytes', egressBytes === null ? 'Set this on the Server Status page when you want to track current egress manually.' : 'Manual value saved from Server Status.', egressBytes === null ? 'unavailable' : 'manual'), ], stats: [], }; } const admin = createClient(supabaseUrl, serviceRoleKey, { auth: { persistSession: false, autoRefreshToken: false }, }); const [ storageUsage, companyCount, projectCount, taskCount, brandBookCount, profileCount, databaseSizeResult, ] = await Promise.all([ getStorageUsage(admin), countRows(admin, 'companies'), countRows(admin, 'projects'), countRows(admin, 'tasks'), countRows(admin, 'brand_books'), countRows(admin, 'profiles'), admin.rpc('get_database_size_bytes'), ]); const databaseBytes = databaseSizeResult.error ? null : toNumber(databaseSizeResult.data); return { configured: true, message: null, quotas: [ makeQuota('storage', 'Storage', storageUsage.totalBytes, SUPABASE_LIMITS.storageBytes, 'bytes', `${storageUsage.totalFiles} file${storageUsage.totalFiles === 1 ? '' : 's'} across ${storageUsage.buckets.length} bucket${storageUsage.buckets.length === 1 ? '' : 's'}.`), makeQuota( 'database', 'Database size', databaseBytes, SUPABASE_LIMITS.databaseBytes, 'bytes', databaseBytes === null ? 'Run the latest Supabase migration to enable live database-size reporting.' : 'Live reading from Supabase.', databaseBytes === null ? 'unavailable' : 'live' ), makeQuota('egress', 'Egress', egressBytes, SUPABASE_LIMITS.egressBytes, 'bytes', egressBytes === null ? 'Set this on the Server Status page when you want to track current egress manually.' : 'Manual value saved from Server Status.', egressBytes === null ? 'unavailable' : 'manual'), ], stats: [ { label: 'Storage Files', value: storageUsage.totalFiles }, { label: 'Buckets', value: storageUsage.buckets.length }, { label: 'Companies', value: companyCount }, { label: 'Projects', value: projectCount }, { label: 'Jobs', value: taskCount }, { label: 'Users', value: profileCount }, { label: 'Brand Books', value: brandBookCount }, ], buckets: storageUsage.buckets.slice(0, 5), }; } async function getVercelUsageSnapshot() { const vercelToken = process.env.VERCEL_TOKEN; const teamId = process.env.VERCEL_TEAM_ID || process.env.VERCEL_ORG_ID || ''; const projectId = process.env.VERCEL_PROJECT_ID || ''; const fastDataTransferBytes = toNumber(process.env.VERCEL_FAST_DATA_TRANSFER_BYTES); const edgeRequests = toNumber(process.env.VERCEL_EDGE_REQUESTS); const functionInvocations = toNumber(process.env.VERCEL_FUNCTION_INVOCATIONS); const activeCpuHours = toNumber(process.env.VERCEL_ACTIVE_CPU_HOURS); const quotas = [ makeQuota('fast-data-transfer', 'Fast Data Transfer', fastDataTransferBytes, VERCEL_LIMITS.fastDataTransferBytes, 'bytes', fastDataTransferBytes === null ? 'Optional: set VERCEL_FAST_DATA_TRANSFER_BYTES to visualize current usage.' : 'Manual value from env.', fastDataTransferBytes === null ? 'unavailable' : 'manual'), makeQuota('edge-requests', 'Edge Requests', edgeRequests, VERCEL_LIMITS.edgeRequests, 'count', edgeRequests === null ? 'Optional: set VERCEL_EDGE_REQUESTS to visualize current usage.' : 'Manual value from env.', edgeRequests === null ? 'unavailable' : 'manual'), makeQuota('function-invocations', 'Function Invocations', functionInvocations, VERCEL_LIMITS.functionInvocations, 'count', functionInvocations === null ? 'Optional: set VERCEL_FUNCTION_INVOCATIONS to visualize current usage.' : 'Manual value from env.', functionInvocations === null ? 'unavailable' : 'manual'), makeQuota('active-cpu', 'Functions Active CPU', activeCpuHours, VERCEL_LIMITS.activeCpuHours, 'hours', activeCpuHours === null ? 'Optional: set VERCEL_ACTIVE_CPU_HOURS to visualize current usage.' : 'Manual value from env.', activeCpuHours === null ? 'unavailable' : 'manual'), ]; if (!vercelToken) { return { configured: false, message: 'Set VERCEL_TOKEN on Vercel to show project and deployment counts.', quotas, stats: [], }; } const authHeaders = { Authorization: `Bearer ${vercelToken}` }; const teamQuery = teamId ? `?teamId=${encodeURIComponent(teamId)}` : ''; const deploymentParams = new URLSearchParams({ limit: '20', since: String(Date.now() - 30 * 24 * 60 * 60 * 1000), }); if (projectId) deploymentParams.set('projectId', projectId); if (teamId) deploymentParams.set('teamId', teamId); const [projectsResponse, deploymentsResponse] = await Promise.all([ fetch(`https://api.vercel.com/v9/projects${teamQuery}`, { headers: authHeaders }), fetch(`https://api.vercel.com/v6/deployments?${deploymentParams.toString()}`, { headers: authHeaders }), ]); let projectCount = null; let deployments30d = null; let deploymentsToday = null; if (projectsResponse.ok) { const projectsJson = await projectsResponse.json(); projectCount = Array.isArray(projectsJson.projects) ? projectsJson.projects.length : null; } if (deploymentsResponse.ok) { const deploymentsJson = await deploymentsResponse.json(); const deployments = Array.isArray(deploymentsJson.deployments) ? deploymentsJson.deployments : []; deployments30d = deployments.length; const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); deploymentsToday = deployments.filter(deployment => { const createdAt = Number(deployment.createdAt || 0); return createdAt >= todayStart.getTime(); }).length; } return { configured: true, message: null, quotas, stats: [ ...(projectCount === null ? [] : [{ label: 'Projects', value: projectCount }]), ...(deployments30d === null ? [] : [{ label: 'Deployments (30d)', value: deployments30d }]), ...(deploymentsToday === null ? [] : [{ label: 'Deployments Today', value: deploymentsToday }]), ], }; } function applyManualOverrides(services, overrides) { if (!overrides) return services; const updateQuota = (serviceKey, quotaId, value, note) => { const service = services[serviceKey]; if (!service) return; service.usage.quotas = (service.usage.quotas || []).map(quota => quota.id === quotaId ? { ...quota, used: toNumber(value), note: toNumber(value) === null ? quota.note : note, source: toNumber(value) === null ? quota.source : 'manual', } : quota ); }; updateQuota('supabase', 'egress', overrides.supabase_egress_bytes, 'Manual value saved from Server Status.'); updateQuota('vercel', 'fast-data-transfer', overrides.vercel_fast_data_transfer_bytes, 'Manual value saved from Server Status.'); updateQuota('vercel', 'edge-requests', overrides.vercel_edge_requests, 'Manual value saved from Server Status.'); updateQuota('vercel', 'function-invocations', overrides.vercel_function_invocations, 'Manual value saved from Server Status.'); updateQuota('vercel', 'active-cpu', overrides.vercel_active_cpu_hours, 'Manual value saved from Server Status.'); return services; } export default async function handler(req, res) { if (!['GET', 'POST'].includes(req.method)) { return json(res, 405, { error: 'Method not allowed' }); } const authHeader = req.headers.authorization || ''; if (!authHeader) { return json(res, 401, { error: 'Missing authorization header' }); } try { const authResult = await requireTeam(authHeader); if (!authResult.ok) { return json(res, authResult.status, { error: authResult.message }); } const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const admin = serviceRoleKey ? createClient(authResult.supabaseUrl, serviceRoleKey, { auth: { persistSession: false, autoRefreshToken: false }, }) : null; if (req.method === 'POST') { if (!admin) return json(res, 500, { error: 'SUPABASE_SERVICE_ROLE_KEY is required to save overrides.' }); const body = typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body || {}); const payload = { id: true, supabase_egress_bytes: toNumber(body.supabase_egress_bytes), vercel_fast_data_transfer_bytes: toNumber(body.vercel_fast_data_transfer_bytes), vercel_edge_requests: toNumber(body.vercel_edge_requests), vercel_function_invocations: toNumber(body.vercel_function_invocations), vercel_active_cpu_hours: toNumber(body.vercel_active_cpu_hours), updated_at: new Date().toISOString(), }; const { error } = await admin.from('server_status_overrides').upsert(payload); if (error) return json(res, 500, { error: error.message }); return json(res, 200, { success: true }); } const overridesResult = admin ? await admin.from('server_status_overrides').select('*').eq('id', true).maybeSingle() : { data: null, error: null }; const [supabaseStatus, vercelStatus, supabaseUsage, vercelUsage] = await Promise.all([ fetchStatusSummary(STATUS_ENDPOINTS.supabase), fetchStatusSummary(STATUS_ENDPOINTS.vercel), getSupabaseUsageSnapshot(authResult.supabaseUrl, overridesResult), getVercelUsageSnapshot(), ]); const services = applyManualOverrides({ supabase: { name: 'Supabase', provider: 'supabase', status: supabaseStatus, usage: supabaseUsage, limits: SUPABASE_LIMITS, }, vercel: { name: 'Vercel', provider: 'vercel', status: vercelStatus, usage: vercelUsage, limits: VERCEL_LIMITS, }, }, overridesResult.data); return json(res, 200, { checkedAt: new Date().toISOString(), overrides: overridesResult.data || null, services, }); } catch (error) { return json(res, 500, { error: error.message || 'Server status failed' }); } }