Session 2026-05-29: profile layout 2-col, file browser copy/paste, fbq-proxy/backfill fns, migrations, UI updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const FB_SOURCE = 'files';
|
||||
const FB_SOURCE = 'srv';
|
||||
|
||||
function normalizePath(path) {
|
||||
const raw = String(path || '/').trim();
|
||||
@@ -31,7 +31,7 @@ function getConfig() {
|
||||
return {
|
||||
url,
|
||||
token: process.env.FILEBROWSER_TOKEN || '',
|
||||
clientRoot: normalizePath(process.env.FILEBROWSER_CLIENT_ROOT || '/fourgebranding/Clients'),
|
||||
clientRoot: normalizePath(process.env.FILEBROWSER_CLIENT_ROOT || '/Clients'),
|
||||
configured: Boolean(url),
|
||||
};
|
||||
}
|
||||
@@ -56,6 +56,37 @@ async function mkdir(config, path) {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async function fbExists(config, path) {
|
||||
try {
|
||||
await fbFetch(config, 'GET', '/api/resources', { params: { path } });
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.status === 404) return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTaskStructure(config, companyName, projectName, taskTitle) {
|
||||
const companyDir = joinPath(config.clientRoot, safeName(companyName));
|
||||
const projectsDir = joinPath(companyDir, 'Projects');
|
||||
const projectDir = joinPath(projectsDir, safeName(projectName));
|
||||
const taskDir = joinPath(projectDir, safeName(taskTitle));
|
||||
const requestInfoDir = joinPath(taskDir, 'Request Info');
|
||||
const workingFilesDir = joinPath(taskDir, 'Working Files');
|
||||
const oldBooksDir = joinPath(taskDir, 'Old Books');
|
||||
|
||||
await mkdir(config, companyDir);
|
||||
await mkdir(config, projectsDir);
|
||||
await mkdir(config, projectDir);
|
||||
await mkdir(config, joinPath(projectDir, '00 Project Files'));
|
||||
await mkdir(config, taskDir);
|
||||
await mkdir(config, requestInfoDir);
|
||||
await mkdir(config, workingFilesDir);
|
||||
await mkdir(config, oldBooksDir);
|
||||
|
||||
return { companyDir, projectDir, taskDir, requestInfoDir };
|
||||
}
|
||||
|
||||
function json(res, status, body) {
|
||||
return res.status(status).json(body);
|
||||
}
|
||||
@@ -80,6 +111,33 @@ export default async function handler(req, res) {
|
||||
const config = getConfig();
|
||||
if (!config.configured || !config.token) return json(res, 200, { ok: true, skipped: 'FileBrowser not configured' });
|
||||
|
||||
const { data: tasks, error: taskError } = await admin
|
||||
.from('tasks')
|
||||
.select(`
|
||||
id, title,
|
||||
project:projects!inner(
|
||||
id, name,
|
||||
company:companies!inner(name)
|
||||
)
|
||||
`);
|
||||
|
||||
if (taskError) return json(res, 500, { error: taskError.message });
|
||||
|
||||
const results = {
|
||||
ensuredTasks: 0,
|
||||
processed: 0,
|
||||
skipped: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
for (const task of tasks || []) {
|
||||
const project = task?.project;
|
||||
const company = project?.company;
|
||||
if (!task?.title || !project?.name || !company?.name) continue;
|
||||
await ensureTaskStructure(config, company.name, project.name, task.title);
|
||||
results.ensuredTasks++;
|
||||
}
|
||||
|
||||
// Fetch all submission files with task/project/company context
|
||||
const { data: rows, error } = await admin
|
||||
.from('submission_files')
|
||||
@@ -120,52 +178,37 @@ export default async function handler(req, res) {
|
||||
}
|
||||
groups.get(key).files.push({ name: row.name, storage_path: row.storage_path });
|
||||
}
|
||||
|
||||
const results = { processed: 0, skipped: 0, errors: [] };
|
||||
|
||||
for (const group of groups.values()) {
|
||||
if (group.files.length === 0) { results.skipped++; continue; }
|
||||
|
||||
const revFolder = `R${String(group.versionNumber).padStart(2, '0')}`;
|
||||
const companyDir = joinPath(config.clientRoot, safeName(group.companyName));
|
||||
const projectDir = joinPath(companyDir, 'Projects', safeName(group.projectName));
|
||||
const taskDir = joinPath(projectDir, safeName(group.taskTitle));
|
||||
const requestInfoDir = joinPath(taskDir, 'Request Info');
|
||||
const { requestInfoDir } = await ensureTaskStructure(config, group.companyName, group.projectName, group.taskTitle);
|
||||
const revDir = joinPath(requestInfoDir, revFolder);
|
||||
|
||||
// Ensure all parent dirs exist
|
||||
await mkdir(config, companyDir);
|
||||
await mkdir(config, joinPath(companyDir, 'Projects'));
|
||||
await mkdir(config, projectDir);
|
||||
await mkdir(config, taskDir);
|
||||
await mkdir(config, requestInfoDir);
|
||||
await mkdir(config, revDir);
|
||||
|
||||
for (const file of group.files) {
|
||||
try {
|
||||
// Get signed URL from Supabase Storage
|
||||
const { data: signed, error: signedError } = await admin.storage
|
||||
const { data: blob, error: downloadError } = await admin.storage
|
||||
.from('submissions')
|
||||
.createSignedUrl(file.storage_path, 60);
|
||||
if (signedError || !signed?.signedUrl) {
|
||||
results.errors.push(`signed url failed: ${file.storage_path}`);
|
||||
.download(file.storage_path);
|
||||
if (downloadError || !blob) {
|
||||
results.errors.push(`download failed: ${file.storage_path}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download file from Supabase Storage
|
||||
const fileRes = await fetch(signed.signedUrl);
|
||||
if (!fileRes.ok) {
|
||||
results.errors.push(`download failed: ${file.name}`);
|
||||
continue;
|
||||
}
|
||||
const fileBuffer = await fileRes.arrayBuffer();
|
||||
const fileBuffer = await blob.arrayBuffer();
|
||||
const form = new FormData();
|
||||
form.append('file', new Blob([fileBuffer]), file.name);
|
||||
|
||||
// Upload to FileBrowser
|
||||
const fbFilePath = joinPath(revDir, file.name);
|
||||
if (await fbExists(config, fbFilePath)) {
|
||||
results.skipped++;
|
||||
continue;
|
||||
}
|
||||
await fbFetch(config, 'POST', '/api/resources', {
|
||||
params: { path: fbFilePath, override: 'true' },
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
body: fileBuffer,
|
||||
params: { path: fbFilePath },
|
||||
body: form,
|
||||
});
|
||||
|
||||
results.processed++;
|
||||
|
||||
Reference in New Issue
Block a user