added restart always to service

This commit is contained in:
2025-12-19 21:51:46 +00:00
parent cf57336c97
commit 56e1131622
4 changed files with 840 additions and 12 deletions
+170 -7
View File
@@ -29,13 +29,167 @@ app.get('/version', (c) => {
}
});
// Serve static files from the frontend directory
app.use('/*', serveStatic({ root: './frontend' }));
// --- Microsoft Graph helpers ---
const getGraphToken = (c?: any) => {
const headerToken = c?.req?.header ? c.req.header('x-graph-token') : '';
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
const token = headerToken || envToken;
console.log('[getGraphToken] Header token:', headerToken ? headerToken.substring(0, 20) + '...' : 'NONE');
console.log('[getGraphToken] Env token:', envToken ? 'SET' : 'NOT SET');
console.log('[getGraphToken] Using:', token ? token.substring(0, 20) + '...' : 'NONE');
return token;
};
// Error handler
app.onError((err, c) => {
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
return c.text('Internal Server Error', 500);
const b64Url = (input: string) =>
Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
type DriveItem = {
id: string;
name: string;
webUrl?: string;
size?: number;
lastModifiedDateTime?: string;
file?: { mimeType?: string };
folder?: { childCount?: number };
parentReference?: { path?: string; driveId?: string };
};
const fetchJson = async (url: string, token: string) => {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Graph ${res.status}: ${text}`);
}
return res.json();
};
const resolveShareLink = async (link: string, token: string) => {
const encoded = `u!${b64Url(link)}`;
const data = await fetchJson(`https://graph.microsoft.com/v1.0/shares/${encoded}/driveItem`, token);
return { driveId: data.parentReference?.driveId as string, itemId: data.id as string };
};
const listChildrenRecursive = async (
driveId: string,
itemId: string,
depth = 0,
maxDepth = 5,
token: string,
): Promise<DriveItem[]> => {
const out: DriveItem[] = [];
const data = await fetchJson(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/children`, token);
for (const child of data.value || []) {
out.push(child as DriveItem);
if (child.folder && depth < maxDepth) {
const nested = await listChildrenRecursive(driveId, child.id, depth + 1, maxDepth, token);
out.push(...nested);
}
}
return out;
};
const searchWithinFolder = async (driveId: string, itemId: string, query: string, token: string): Promise<DriveItem[]> => {
const data = await fetchJson(
`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/search(q='${encodeURIComponent(query)}')`,
token,
);
return (data.value || []) as DriveItem[];
};
const filterByCategory = (items: DriveItem[], category?: string) => {
if (!category) return items;
const nameHas = (name: string, words: string[]) => words.some((w) => name.includes(w));
const lc = (s: string) => (s || '').toLowerCase();
const contractWords = ['contract', 'estimate', 'proposal', 'bid', 'award', 'agreement', 'sow'];
const planWords = ['plan', 'drawing', 'dwg', 'pdf', 'sheet', 'layout'];
return items.filter((i) => {
const n = lc(i.name);
if (category === 'contracts') return nameHas(n, contractWords);
if (category === 'plans') return nameHas(n, planWords);
return true;
});
};
// List/search job files via Graph using a shared folder link
app.get('/api/job-files', async (c) => {
try {
console.log('[job-files] Request received');
const token = getGraphToken(c);
if (!token) {
console.log('[job-files] No token found');
return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
}
console.log('[job-files] Token found:', token.substring(0, 20) + '...');
const link = c.req.query('link');
const q = c.req.query('q') || '';
const category = c.req.query('category') || '';
if (!link) return c.json({ error: 'link required' }, 400);
console.log('[job-files] Link:', link);
const { driveId, itemId } = await resolveShareLink(link, token);
let items: DriveItem[] = [];
if (q) {
items = await searchWithinFolder(driveId, itemId, q, token);
} else {
// Walk subfolders up to depth 5
items = await listChildrenRecursive(driveId, itemId, 0, 5, token);
}
const filtered = filterByCategory(items, category);
const mapped = filtered.map((i) => ({
id: i.id,
name: i.name,
url: i.webUrl,
driveId: i.parentReference?.driveId || driveId,
size: i.size,
modified: i.lastModifiedDateTime,
contentType: i.file?.mimeType,
isFolder: Boolean(i.folder),
path: i.parentReference?.path || '',
}));
return c.json({ items: mapped, total: mapped.length, source: q ? 'search' : 'walk', driveId });
} catch (err) {
const message = (err as Error)?.message || String(err);
logLine('logs/error.log', `/api/job-files error: ${message}`);
return c.json({ error: message }, 500);
}
});
// Stream file content via Graph to avoid CSP issues for inline preview
app.get('/api/job-file-content', async (c) => {
try {
const token = getGraphToken(c);
if (!token) return c.json({ error: 'GRAPH_TOKEN missing' }, 500);
const driveId = c.req.query('driveId');
const itemId = c.req.query('itemId');
if (!driveId || !itemId) return c.json({ error: 'driveId and itemId required' }, 400);
const res = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
return c.json({ error: `Graph ${res.status}: ${text}` }, res.status);
}
const headers: Record<string, string> = {};
const ct = res.headers.get('content-type');
const cd = res.headers.get('content-disposition');
if (ct) headers['Content-Type'] = ct;
if (cd) headers['Content-Disposition'] = cd;
return new Response(res.body, { status: 200, headers });
} catch (err) {
const message = (err as Error)?.message || String(err);
logLine('logs/error.log', `/api/job-file-content error: ${message}`);
return c.json({ error: message }, 500);
}
});
// Mutation logging endpoint
@@ -52,7 +206,16 @@ app.post('/log-change', async (c) => {
}
});
const PORT = Number(process.env.PORT || 3000);
// Serve static files from the frontend directory (must come after API routes)
app.use('/*', serveStatic({ root: './frontend' }));
// Error handler
app.onError((err, c) => {
logLine('logs/error.log', `Unhandled error: ${err?.message || err}`);
return c.text('Internal Server Error', 500);
});
const PORT = Number(process.env.PORT || 3003);
logLine('logs/server.log', `Starting frontend server on port ${PORT}`);