Fix file sharing load speed and move error; misc updates

- Remove recursive directory size calculations (single Seafile API call per list)
- Remove 'Used in this location' usage display
- Fix move using v2 per-type endpoints instead of broken batch endpoint
- Send entry type from frontend for correct move routing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-13 14:20:38 -04:00
parent c9e7816e28
commit eee0885811
117 changed files with 17592 additions and 4057 deletions
+98
View File
@@ -0,0 +1,98 @@
import crypto from 'crypto';
import { createClient } from '@supabase/supabase-js';
function json(res, status, body) {
res.status(status).setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-store');
res.send(JSON.stringify(body));
}
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 };
}
function getKey() {
const secret = process.env.PASSWORD_VAULT_KEY || '';
if (!secret) throw new Error('PASSWORD_VAULT_KEY is not configured on Vercel.');
return Buffer.from(secret, 'base64');
}
export default async function handler(req, res) {
if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' });
try {
const authHeader = req.headers.authorization || '';
if (!authHeader) return json(res, 401, { error: 'No authorization header' });
const auth = await requireTeam(authHeader);
if (!auth.ok) return json(res, auth.status, { error: auth.message });
const { action, plaintext, ciphertext, iv } = req.body || {};
const key = getKey();
if (action === 'encrypt') {
if (!plaintext) return json(res, 400, { error: 'plaintext required' });
const ivBuffer = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, ivBuffer);
const encrypted = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
const packed = Buffer.concat([encrypted, tag]);
return json(res, 200, {
ciphertext: packed.toString('base64'),
iv: ivBuffer.toString('base64'),
});
}
if (action === 'decrypt') {
if (!ciphertext || !iv) return json(res, 400, { error: 'ciphertext and iv required' });
const raw = Buffer.from(String(ciphertext), 'base64');
const ivBuffer = Buffer.from(String(iv), 'base64');
const encrypted = raw.subarray(0, raw.length - 16);
const tag = raw.subarray(raw.length - 16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, ivBuffer);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return json(res, 200, { plaintext: decrypted.toString('utf8') });
}
return json(res, 400, { error: 'Invalid action' });
} catch (error) {
return json(res, 500, { error: error.message || 'Unexpected server error' });
}
}
+496
View File
@@ -0,0 +1,496 @@
import { createClient } from '@supabase/supabase-js';
const DIRECTORY_USAGE_CACHE_TTL_MS = 2 * 60 * 1000;
const directoryUsageCache = new Map();
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 normalizeBaseUrl(url) {
return String(url || '').trim().replace(/\/+$/, '');
}
function normalizePath(path) {
const raw = String(path || '/').trim();
const parts = raw.split('/').filter(Boolean);
const clean = [];
for (const part of parts) {
if (part === '.' || part === '') continue;
if (part === '..') throw new Error('Invalid path');
clean.push(part);
}
return `/${clean.join('/')}`;
}
function joinPath(...parts) {
return normalizePath(parts.join('/'));
}
function safeName(value, fallback) {
const cleaned = String(value || '')
.trim()
.replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-')
.replace(/\s+/g, ' ')
.replace(/^-+|-+$/g, '');
return cleaned || fallback;
}
function fillTemplate(template, profile) {
const name = safeName(profile.name, profile.id);
const companyName = safeName(profile.company?.name, name);
const email = safeName(profile.email, profile.id);
const emailName = safeName(String(profile.email || '').split('@')[0], profile.id);
return String(template || '')
.replaceAll('{id}', profile.id)
.replaceAll('{name}', name)
.replaceAll('{companyName}', companyName)
.replaceAll('{email}', email)
.replaceAll('{emailName}', emailName);
}
function getConfig() {
const serverUrl = normalizeBaseUrl(process.env.SEAFILE_SERVER_URL);
const apiToken = process.env.SEAFILE_API_TOKEN;
const repoId = process.env.SEAFILE_REPO_ID;
return {
serverUrl,
webUrl: normalizeBaseUrl(process.env.SEAFILE_WEB_URL || serverUrl),
apiToken,
repoId,
teamRoot: normalizePath(process.env.SEAFILE_TEAM_ROOT_PATH || '/'),
externalRoot: normalizePath(process.env.SEAFILE_EXTERNAL_ROOT_PATH || '/Subcontractors'),
externalTemplate: process.env.SEAFILE_EXTERNAL_FOLDER_TEMPLATE || '{name}',
clientRoot: normalizePath(process.env.SEAFILE_CLIENT_ROOT_PATH || '/Clients'),
clientTemplate: process.env.SEAFILE_CLIENT_FOLDER_TEMPLATE || '{companyName}',
configured: Boolean(serverUrl && apiToken && repoId),
};
}
async function createCallerClient(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.');
}
return createClient(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: false, autoRefreshToken: false },
global: { headers: { Authorization: authHeader } },
});
}
async function requirePortalUser(authHeader) {
const callerClient = await createCallerClient(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('id, name, role, company:companies(id, name)')
.eq('id', userData.user.id)
.single();
if (profileError) {
return { ok: false, status: 500, message: profileError.message };
}
if (!['team', 'external', 'client'].includes(profile?.role)) {
return { ok: false, status: 403, message: 'Forbidden' };
}
return {
ok: true,
callerClient,
profile: {
...profile,
email: userData.user.email,
},
};
}
function getUserRoot(config, profile) {
if (profile.role === 'team') return config.teamRoot;
if (profile.role === 'client') {
const templated = fillTemplate(config.clientTemplate, profile);
if (templated.startsWith('/')) return normalizePath(templated);
return joinPath(config.clientRoot, templated);
}
const templated = fillTemplate(config.externalTemplate, profile);
if (templated.startsWith('/')) return normalizePath(templated);
return joinPath(config.externalRoot, templated);
}
function resolveSeafilePath(config, profile, requestedPath = '/') {
const root = getUserRoot(config, profile);
const virtualPath = normalizePath(requestedPath);
return {
root,
virtualPath,
seafilePath: joinPath(root, virtualPath),
};
}
async function seafileRequest(config, endpoint, options = {}) {
const response = await fetch(`${config.serverUrl}${endpoint}`, {
...options,
headers: {
Authorization: `Token ${config.apiToken}`,
Accept: 'application/json',
...(options.headers || {}),
},
});
const text = await response.text();
let body = text;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text;
}
if (!response.ok) {
const message = typeof body === 'object' && body?.error_msg ? body.error_msg : text || `Seafile returned ${response.status}`;
const error = new Error(message);
error.status = response.status;
throw error;
}
return body;
}
async function createSeafileFolder(config, path) {
const body = new URLSearchParams({ operation: 'mkdir', create_parents: 'true' });
await seafileRequest(config, `/api2/repos/${encodeURIComponent(config.repoId)}/dir/?p=${encodeURIComponent(path)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
}
async function createSeafileFolderIfMissing(config, path) {
try {
await createSeafileFolder(config, path);
return true;
} catch (error) {
const message = String(error.message || '').toLowerCase();
if (error.status === 400 || message.includes('already') || message.includes('exist')) return false;
throw error;
}
}
function getCachedDirectoryUsage(cacheKey) {
const cached = directoryUsageCache.get(cacheKey);
if (!cached) return null;
if ((Date.now() - cached.timestamp) > DIRECTORY_USAGE_CACHE_TTL_MS) {
directoryUsageCache.delete(cacheKey);
return null;
}
return cached.bytes;
}
function setCachedDirectoryUsage(cacheKey, bytes) {
directoryUsageCache.set(cacheKey, {
bytes: Number(bytes) || 0,
timestamp: Date.now(),
});
}
async function listDirectoryEntries(config, path) {
const entries = await seafileRequest(
config,
`/api2/repos/${encodeURIComponent(config.repoId)}/dir/?p=${encodeURIComponent(path)}`
);
return Array.isArray(entries) ? entries : [];
}
async function getDirectoryUsageBytes(config, path, prefetchedEntries = null) {
const normalizedPath = normalizePath(path);
const cacheKey = `${config.repoId}:${normalizedPath}`;
const cached = getCachedDirectoryUsage(cacheKey);
if (cached != null) return cached;
const entries = prefetchedEntries || await listDirectoryEntries(config, normalizedPath);
let total = 0;
for (const entry of entries) {
if (entry.type === 'file') {
total += Number(entry.size || 0);
continue;
}
if (entry.type === 'dir') {
total += await getDirectoryUsageBytes(config, joinPath(normalizedPath, entry.name));
}
}
setCachedDirectoryUsage(cacheKey, total);
return total;
}
function clearDirectoryUsageCache(config, path) {
const normalizedPath = normalizePath(path);
const prefixes = [];
let cursor = normalizedPath;
while (true) {
prefixes.push(`${config.repoId}:${cursor}`);
if (cursor === '/') break;
cursor = parentDir(cursor);
}
for (const key of prefixes) {
directoryUsageCache.delete(key);
}
}
function entryPath(parent, name) {
return joinPath(parent, name);
}
function parentDir(path) {
const normalized = normalizePath(path);
const parts = normalized.split('/').filter(Boolean);
parts.pop();
return `/${parts.join('/')}`;
}
function basename(path) {
const parts = normalizePath(path).split('/').filter(Boolean);
return parts[parts.length - 1] || '';
}
async function syncManagedFolders(config, auth) {
if (auth.profile.role !== 'team') {
const error = new Error('Team only');
error.status = 403;
throw error;
}
const [{ data: companies, error: companiesError }, { data: externals, error: externalsError }] = await Promise.all([
auth.callerClient.from('companies').select('id, name').order('name'),
auth.callerClient.from('profiles').select('id, name, email, role').eq('role', 'external').order('name'),
]);
if (companiesError) throw new Error(companiesError.message);
if (externalsError) throw new Error(externalsError.message);
const folderPaths = [
config.clientRoot,
config.externalRoot,
...(companies || []).map(company => {
const profile = { id: company.id, name: company.name, email: '', company };
const templated = fillTemplate(config.clientTemplate, profile);
return templated.startsWith('/') ? normalizePath(templated) : joinPath(config.clientRoot, templated);
}),
...(externals || []).map(profile => {
const templated = fillTemplate(config.externalTemplate, profile);
return templated.startsWith('/') ? normalizePath(templated) : joinPath(config.externalRoot, templated);
}),
];
const uniquePaths = [...new Set(folderPaths)].filter(path => path !== '/');
let created = 0;
for (const path of uniquePaths) {
if (await createSeafileFolderIfMissing(config, path)) created += 1;
}
return {
created,
checked: uniquePaths.length,
clients: companies?.length || 0,
subcontractors: externals?.length || 0,
};
}
export default async function handler(req, res) {
try {
const authHeader = req.headers.authorization || '';
if (!authHeader) return json(res, 401, { error: 'No authorization header' });
const auth = await requirePortalUser(authHeader);
if (!auth.ok) return json(res, auth.status, { error: auth.message });
const config = getConfig();
if (!config.configured) {
return json(res, 200, {
configured: false,
error: 'Seafile is not configured yet.',
requiredEnv: ['SEAFILE_SERVER_URL', 'SEAFILE_API_TOKEN', 'SEAFILE_REPO_ID'],
});
}
const action = req.query.action || (req.method === 'GET' ? 'list' : '');
const requestedPath = req.query.path || req.body?.path;
const resolved = resolveSeafilePath(config, auth.profile, requestedPath || '/');
const invalidateUsage = req.query.invalidateUsage === '1';
if (req.method === 'POST' && action === 'sync-folders') {
const result = await syncManagedFolders(config, auth);
return json(res, 200, { success: true, ...result });
}
if (req.method === 'GET' && action === 'config') {
return json(res, 200, {
configured: true,
role: auth.profile.role,
root: resolved.root,
webUrl: config.webUrl,
});
}
if (req.method === 'GET' && action === 'list') {
if (invalidateUsage) clearDirectoryUsageCache(config, resolved.seafilePath);
let entries;
try {
entries = await listDirectoryEntries(config, resolved.seafilePath);
} catch (error) {
if (!['external', 'client'].includes(auth.profile.role) || resolved.virtualPath !== '/') throw error;
await createSeafileFolder(config, resolved.root);
entries = await listDirectoryEntries(config, resolved.seafilePath);
}
const normalizedEntries = (Array.isArray(entries) ? entries : []).map((item) => {
const itemPath = entryPath(resolved.virtualPath, item.name);
return {
id: item.id,
name: item.name,
type: item.type,
size: item.type === 'file' ? Number(item.size || 0) : 0,
aggregateSize: item.type === 'file' ? Number(item.size || 0) : null,
mtime: item.mtime || null,
permission: item.permission || null,
path: itemPath,
};
}).sort((a, b) => {
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
return a.name.localeCompare(b.name);
});
return json(res, 200, {
configured: true,
path: resolved.virtualPath,
canGoUp: resolved.virtualPath !== '/',
parentPath: parentDir(resolved.virtualPath),
entries: normalizedEntries,
webUrl: config.webUrl,
});
}
if (req.method === 'GET' && action === 'download') {
const url = await seafileRequest(
config,
`/api2/repos/${encodeURIComponent(config.repoId)}/file/?p=${encodeURIComponent(resolved.seafilePath)}&reuse=1`
);
return json(res, 200, { url });
}
if (req.method === 'POST' && action === 'mkdir') {
const folderName = safeName(req.body?.name, '');
if (!folderName) return json(res, 400, { error: 'Folder name is required.' });
const folderPath = joinPath(resolved.seafilePath, folderName);
await createSeafileFolder(config, folderPath);
clearDirectoryUsageCache(config, resolved.seafilePath);
return json(res, 200, { success: true });
}
if (req.method === 'POST' && action === 'upload-link') {
const uploadLink = await seafileRequest(
config,
`/api2/repos/${encodeURIComponent(config.repoId)}/upload-link/?p=${encodeURIComponent(resolved.seafilePath)}`
);
return json(res, 200, {
uploadLink: typeof uploadLink === 'string' ? uploadLink : String(uploadLink || ''),
parentDir: resolved.seafilePath,
});
}
if (req.method === 'POST' && action === 'move') {
const srcPath = req.body?.srcPath;
const dstDir = req.body?.dstDir;
if (!srcPath || !dstDir) return json(res, 400, { error: 'srcPath and dstDir are required.' });
const resolvedSrc = resolveSeafilePath(config, auth.profile, srcPath);
const resolvedDst = resolveSeafilePath(config, auth.profile, dstDir);
const itemName = basename(resolvedSrc.seafilePath);
const srcDir = parentDir(resolvedSrc.seafilePath);
if (!itemName) return json(res, 400, { error: 'Cannot move root.' });
const type = req.body?.type || 'file';
const endpoint = type === 'dir'
? `/api2/repos/${encodeURIComponent(config.repoId)}/dir/?p=${encodeURIComponent(resolvedSrc.seafilePath)}`
: `/api2/repos/${encodeURIComponent(config.repoId)}/file/?p=${encodeURIComponent(resolvedSrc.seafilePath)}`;
const body = new URLSearchParams({
operation: 'move',
dst_repo: config.repoId,
dst_dir: resolvedDst.seafilePath,
});
await seafileRequest(config, endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
clearDirectoryUsageCache(config, srcDir);
clearDirectoryUsageCache(config, resolvedDst.seafilePath);
return json(res, 200, { success: true });
}
if (req.method === 'DELETE' && action === 'delete') {
const type = req.query.type || req.body?.type;
if (!['file', 'dir'].includes(type)) return json(res, 400, { error: 'Valid item type is required.' });
const itemName = basename(resolved.seafilePath);
const itemParent = parentDir(resolved.seafilePath);
if (!itemName) return json(res, 400, { error: 'Cannot delete the root folder.' });
const body = new URLSearchParams({
file_names: itemName,
});
await seafileRequest(
config,
`/api2/repos/${encodeURIComponent(config.repoId)}/fileops/delete/?p=${encodeURIComponent(itemParent)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
}
);
clearDirectoryUsageCache(config, itemParent);
return json(res, 200, { success: true });
}
return json(res, 405, { error: 'Method not allowed' });
} catch (error) {
return json(res, error.status || 500, { error: error.message || 'Unexpected Seafile error' });
}
}
+446
View File
@@ -0,0 +1,446 @@
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' });
}
}