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
+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' });
}
}