Server
+Checking...
+diff --git a/index.html b/index.html index 9c64121..e5af892 100644 --- a/index.html +++ b/index.html @@ -93,11 +93,17 @@
- Prism Notes
-
+ Prism Notes
+Clean baseline UI with PocketBase auth, OneNote delegated auth, note submit, and OneNote proof actions.
@@ -167,13 +173,14 @@Last updated: March 25, 2026
+ ++ This EULA is a legal agreement between you and Cardoza Construction for use of the Prism Notes + application and related services. +
+ ++ Subject to your compliance with this agreement, Cardoza Construction grants you a limited, + non-exclusive, non-transferable, revocable license to use Prism Notes for authorized business use. +
+ ++ Prism Notes and all related intellectual property remain the exclusive property of Cardoza Construction + and its licensors. +
+ ++ You are responsible for ensuring your use of third-party integrations complies with applicable + terms and permissions. Use of Prism Notes is also governed by the Privacy Policy. +
+ ++ Prism Notes is provided "as is" without warranties of any kind, to the extent permitted by law. +
+ ++ To the maximum extent permitted by law, Cardoza Construction is not liable for indirect, + incidental, special, consequential, or punitive damages arising from use of the application. +
+ ++ This license may terminate if you violate this agreement. Upon termination, you must stop using + Prism Notes. +
+ ++ For legal questions, contact: admin@ccllc.pro +
+Last updated: March 25, 2026
+ ++ Prism Notes is operated by Cardoza Construction. This policy describes how Prism Notes collects, + uses, and protects information when you use the application and connected integrations. +
+ ++ We do not sell personal data. Data is shared only with service providers and integrations required to + deliver application features, or as required by law. +
+ ++ Data is retained only as long as required to operate Prism Notes, meet legal obligations, and enforce + agreements. +
+ ++ We use reasonable administrative and technical safeguards to protect data. No method of transmission + or storage is guaranteed to be 100% secure. +
+ ++ For privacy questions, contact: admin@ccllc.pro +
+${line || ' '}
`) + .join(''); + + const payload: Record]*>([\s\S]*?)<\/pre>/gi;
+ let preMatch: RegExpExecArray | null = null;
+ while ((preMatch = preRegex.exec(blockHtml))) {
+ const code = toPlainText(preMatch[1] || '').trim();
+ if (code) codeBlocks.push(code.slice(0, 400));
+ }
+
+ const plain = toPlainText(blockHtml);
+ const fencedRegex = /```[\w-]*\n([\s\S]*?)```/g;
+ let fencedMatch: RegExpExecArray | null = null;
+ while ((fencedMatch = fencedRegex.exec(plain))) {
+ const code = String(fencedMatch[1] || '').trim();
+ if (code) codeBlocks.push(code.slice(0, 400));
+ }
+
+ return codeBlocks;
+}
+
+function extractPageNoteBlocks(pageHtml: string): Array<{ noteId: string; syncedAt: string; preview: string; codeBlocks: string[]; targetId: string }> {
+ const noteBlocks: Array<{ noteId: string; syncedAt: string; preview: string; codeBlocks: string[]; targetId: string }> = [];
+ const divRegex = /]*)\bdata-prism-notes-note-id\s*=\s*(["'])([^"']+)\2([^>]*)>([\s\S]*?)<\/div>/gi;
+ let match: RegExpExecArray | null = null;
+
+ while ((match = divRegex.exec(pageHtml))) {
+ const attrs = `${match[1] || ''} ${match[4] || ''}`;
+ const blockHtml = match[5] || '';
+ const generatedIdMatch = attrs.match(/\bid="([^"]+)"/i);
+ const dataIdMatch = attrs.match(/\bdata-id="([^"]+)"/i);
+ const syncedAtMatch = attrs.match(/data-prism-notes-synced-at="([^"]*)"/i);
+
+ noteBlocks.push({
+ noteId: decodeHtmlEntities(match[3] || '').trim(),
+ syncedAt: decodeHtmlEntities(syncedAtMatch?.[1] || '').trim(),
+ preview: toPlainText(blockHtml).slice(0, 220),
+ codeBlocks: extractCodeBlocks(blockHtml),
+ targetId: decodeHtmlEntities(generatedIdMatch?.[1] || dataIdMatch?.[1] || '').trim(),
+ });
+ }
+
+ if (noteBlocks.length > 0) {
+ return noteBlocks;
+ }
+
+ const structuredPlain = toStructuredPlainText(pageHtml);
+ const noteIdRegex = /note_id\s*:\s*([a-zA-Z0-9._:-]+)/gi;
+ const matches = Array.from(structuredPlain.matchAll(noteIdRegex));
+
+ if (matches.length > 0) {
+ for (let i = 0; i < matches.length; i++) {
+ const match = matches[i];
+ const noteId = decodeHtmlEntities(match[1] || '').trim();
+ if (!noteId) continue;
+
+ const start = match.index ?? 0;
+ const end = i + 1 < matches.length ? (matches[i + 1].index ?? structuredPlain.length) : structuredPlain.length;
+ const segment = structuredPlain.slice(start, end).trim();
+
+ const codeBlocks: string[] = [];
+ const fencedRegex = /```[\w-]*\n([\s\S]*?)```/g;
+ let fencedMatch: RegExpExecArray | null = null;
+ while ((fencedMatch = fencedRegex.exec(segment))) {
+ const code = String(fencedMatch[1] || '').trim();
+ if (code) codeBlocks.push(code.slice(0, 400));
+ }
+
+ noteBlocks.push({
+ noteId,
+ syncedAt: '',
+ preview: segment.slice(0, 220),
+ codeBlocks,
+ targetId: findNearestGeneratedId(pageHtml, pageHtml.search(new RegExp(`note_id\\s*:\\s*${noteId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i'))),
+ });
+ }
+ return noteBlocks;
+ }
+
+ const chunks = pageHtml.split(/\s*---\s*<\/p>/gi);
+ for (const chunk of chunks) {
+ const plain = toPlainText(chunk);
+ const noteIdMatch = plain.match(/note_id\s*:\s*([a-zA-Z0-9._:-]+)/i);
+ if (!noteIdMatch) continue;
+
+ const noteId = decodeHtmlEntities(noteIdMatch[1] || '').trim();
+ if (!noteId) continue;
+
+ const rawIndex = pageHtml.indexOf(chunk);
+ noteBlocks.push({
+ noteId,
+ syncedAt: '',
+ preview: plain.slice(0, 220),
+ codeBlocks: extractCodeBlocks(chunk),
+ targetId: findNearestGeneratedId(pageHtml, rawIndex),
+ });
+ }
+
+ return noteBlocks;
+}
+
+function findRecordFieldName(record: Record, patterns: RegExp[]): string | null {
+ const keys = Object.keys(record || {});
+ for (const pattern of patterns) {
+ const match = keys.find((key) => pattern.test(key));
+ if (match) return match;
+ }
+ return null;
+}
+
// ---------------------------------------------------------------------------
// MSAL app-only (Graph status check)
// ---------------------------------------------------------------------------
@@ -116,6 +419,32 @@ async function resolveSiteId(delegatedToken: string): Promise {
app.get('/health', (c) => c.json({ ok: true, pbDB: PB_BASE_URL }));
+// ---------------------------------------------------------------------------
+// Intuit app setup URLs (for QuickBooks Time / Intuit app configuration)
+// ---------------------------------------------------------------------------
+app.get('/integrations/intuit/launch', async (c) => {
+ const html = await Bun.file(new URL('./index.html', import.meta.url)).text();
+ return c.html(html);
+});
+
+app.get('/integrations/intuit/connect', (c) => {
+ return c.json({
+ success: true,
+ message: 'Intuit connect endpoint is reachable. OAuth flow wiring can be added here.',
+ });
+});
+
+app.get('/integrations/intuit/reconnect', (c) => {
+ return c.redirect('/integrations/intuit/connect', 302);
+});
+
+app.all('/integrations/intuit/disconnect', (c) => {
+ return c.json({
+ success: true,
+ message: 'Intuit disconnect endpoint is reachable. Token revocation/disconnect flow can be added here.',
+ });
+});
+
app.get('/api/auth/pocketbase-config', (c) => {
if (!PB_BASE_URL) {
return c.json({ success: false, message: 'Missing PB_URL or PB_DB' }, 500);
@@ -144,6 +473,133 @@ app.get('/api/auth/msal-config', (c) => {
});
});
+// GET /api/tasks/users
+// Requires: x-pb-token
+app.get('/api/tasks/users', async (c) => {
+ const pbToken = c.req.header('x-pb-token');
+ if (!pbToken) return c.json({ success: false, message: 'Missing x-pb-token' }, 401);
+
+ try {
+ await validatePbToken(pbToken);
+ } catch {
+ return c.json({ success: false, message: 'Invalid PocketBase token' }, 401);
+ }
+
+ try {
+ const me = pb.authStore.model;
+ let users: any[] = [];
+ let warning = '';
+ try {
+ users = await pb.collection(PB_AUTH_COLLECTION).getFullList({
+ sort: 'email',
+ fields: 'id,email,name,username',
+ });
+ } catch (listErr) {
+ warning = 'Users list is restricted by PocketBase rules; showing current user only.';
+ users = me ? [me] : [];
+ }
+
+ return c.json({
+ success: true,
+ me: {
+ id: me?.id || '',
+ email: me?.email || '',
+ name: (me?.name || me?.username || me?.email || 'Me') as string,
+ },
+ users: users.map((user: any) => ({
+ id: user.id,
+ email: user.email || '',
+ name: user.name || user.username || user.email || user.id,
+ })),
+ warning: warning || undefined,
+ });
+ } catch (e) {
+ return c.json({ success: false, message: (e as Error)?.message || 'Failed to load users' }, 500);
+ } finally {
+ clearPbToken();
+ }
+});
+
+// GET /api/tasks/list?userId=
+// Requires: x-pb-token
+app.get('/api/tasks/list', async (c) => {
+ const pbToken = c.req.header('x-pb-token');
+ if (!pbToken) return c.json({ success: false, message: 'Missing x-pb-token' }, 401);
+
+ try {
+ await validatePbToken(pbToken);
+ } catch {
+ return c.json({ success: false, message: 'Invalid PocketBase token' }, 401);
+ }
+
+ try {
+ const me = pb.authStore.model;
+ const meId = String(me?.id || '').trim();
+ const requestedUserId = String(c.req.query('userId') || '').trim();
+ const targetUserId = requestedUserId || meId;
+ if (!targetUserId) return c.json({ success: false, message: 'No target user available' }, 400);
+
+ const fetchTasks = async (userId: string) => {
+ const filter = `user = "${userId.replace(/"/g, '\\"')}"`;
+ return pb.collection(PB_TASKS_COLLECTION).getFullList({
+ filter,
+ sort: '-startDate,-created',
+ expand: 'user',
+ fields: 'id,user,title,startDate,dueDate,priority,completed,content,size,status,expand.user.id,expand.user.email,expand.user.name,expand.user.username',
+ });
+ };
+
+ let records: any[] = [];
+ let warning = '';
+ let effectiveUserId = targetUserId;
+ try {
+ records = await fetchTasks(targetUserId);
+ } catch (listErr) {
+ if (targetUserId !== meId && meId) {
+ try {
+ records = await fetchTasks(meId);
+ effectiveUserId = meId;
+ warning = 'Requested user tasks are restricted by PocketBase rules; showing your tasks instead.';
+ } catch {
+ records = [];
+ effectiveUserId = meId;
+ warning = 'Task list is restricted by PocketBase rules; no accessible tasks for current user.';
+ }
+ } else {
+ records = [];
+ effectiveUserId = meId || targetUserId;
+ warning = 'Task list is restricted by PocketBase rules; no accessible tasks for current user.';
+ }
+ }
+
+ return c.json({
+ success: true,
+ userId: effectiveUserId,
+ count: records.length,
+ warning: warning || undefined,
+ tasks: records.map((task: any) => ({
+ id: task.id,
+ user: task.user || task.expand?.user?.id || null,
+ userDisplay: task.expand?.user
+ ? (task.expand.user.name || task.expand.user.username || task.expand.user.email || task.expand.user.id)
+ : null,
+ title: task.title ?? '',
+ startDate: task.startDate ?? null,
+ dueDate: task.dueDate ?? null,
+ priority: task.priority ?? null,
+ completed: !!task.completed,
+ content: task.content ?? null,
+ size: task.size ?? null,
+ status: task.status ?? null,
+ })),
+ });
+ } catch (e) {
+ return c.json({ success: false, message: (e as Error)?.message || 'Failed to load tasks' }, 500);
+ } finally {
+ clearPbToken();
+ }
+});
+
app.get('/api/graph/status', async (c) => {
try {
const t = await getGraphToken();
@@ -197,19 +653,53 @@ app.get('/api/onenote/section-pages', async (c) => {
}
});
+// GET /api/onenote/page-note-blocks?pageId=...
+// Requires: Authorization: Bearer
+app.get('/api/onenote/page-note-blocks', async (c) => {
+ try {
+ const delegatedToken = getBearerToken(c.req.header('authorization'));
+ if (!delegatedToken) return c.json({ success: false, message: 'Missing delegated token' }, 401);
+
+ const pageId = String(c.req.query('pageId') || '').trim();
+ if (!pageId) return c.json({ success: false, message: 'Missing pageId' }, 400);
+
+ const siteId = await resolveSiteId(delegatedToken);
+ const resp = await fetch(
+ `https://graph.microsoft.com/v1.0/sites/${siteId}/onenote/pages/${encodeURIComponent(pageId)}/content?includeIDs=true`,
+ {
+ headers: {
+ Authorization: `Bearer ${delegatedToken}`,
+ Accept: 'text/html',
+ },
+ },
+ );
+
+ const html = await resp.text();
+ if (!resp.ok) {
+ return c.json({ success: false, siteId, pageId, error: html }, resp.status as any);
+ }
+
+ const noteBlocks = extractPageNoteBlocks(html);
+ return c.json({ success: true, siteId, pageId, noteBlocks });
+ } catch (e) {
+ return c.json({ success: false, message: (e as Error)?.message }, 500);
+ }
+});
+
// ---------------------------------------------------------------------------
// POST /api/onenote/append
// Requires: x-pb-token (PocketBase session) + Authorization: Bearer
// Body: {
// noteId, title, bodyPlain, noteType?, userEmail?, jobNumber?,
-// sectionId?, pageId?, pageTitle?, syncMode?: 'add'|'edit', targetNoteId?
+// sectionId?, pageId?, pageTitle?, syncMode?: 'add_separate'|'add_to'|'edit', targetNoteId?, targetObjectId?
// }
//
// Strategy:
// 1. Resolve site ID via GET /sites/{host}:{path}
// 2. If syncMode=edit: PATCH replace note block in known page by noteId
-// 3. If syncMode=add and pageId: PATCH append to known page
-// 4. If syncMode=add and no pageId: POST create new page in known section
+// 3. If syncMode=add_to: PATCH append content to existing note block by noteId
+// 4. If syncMode=add_separate and pageId: PATCH append a new note block to page
+// 5. If syncMode=add_separate and no pageId: POST create new page in section
// ---------------------------------------------------------------------------
app.post('/api/onenote/append', async (c) => {
try {
@@ -235,8 +725,11 @@ app.post('/api/onenote/append', async (c) => {
const userEmail = String(body?.userEmail || 'unknown').trim();
const jobNumber = String(body?.jobNumber || '').trim();
const pageTitle = String(body?.pageTitle || title || ONENOTE_TARGET.pageTitle || 'Prism Notes').trim();
- const syncMode = String(body?.syncMode || 'add').trim().toLowerCase() === 'edit' ? 'edit' : 'add';
+ const requestedMode = String(body?.syncMode || 'add_to').trim().toLowerCase();
+ const syncMode = requestedMode === 'add_to' ? 'add_to' : requestedMode === 'edit' ? 'edit' : 'add_separate';
const targetNoteId = String(body?.targetNoteId || noteId || '').trim();
+ const targetObjectId = String(body?.targetObjectId || '').trim();
+ const effectiveNoteId = syncMode === 'add_separate' ? noteId : targetNoteId;
if (!noteId) return c.json({ success: false, message: 'Missing noteId' }, 400);
if (!bodyPlain) return c.json({ success: false, message: 'Missing bodyPlain' }, 400);
@@ -248,10 +741,26 @@ app.post('/api/onenote/append', async (c) => {
`type: ${escapeHtml(noteType)}`,
`user: ${escapeHtml(userEmail)}`,
jobNumber ? `job: ${escapeHtml(jobNumber)}` : '',
- `note_id: ${escapeHtml(noteId)}`,
+ `note_id: ${escapeHtml(effectiveNoteId)}`,
].filter(Boolean).join(' | ');
- const appendHtml = `${headerLine}
${safeBody}
---
`;
+ const noteDataId = `pn-${escapeHtml(effectiveNoteId)}`;
+ const appendHtml = `${headerLine}
${safeBody}
---
`;
+ const separateNoteHtml = [
+ ``,
+ '',
+ '',
+ ``,
+ 'Prism Notes Entry
',
+ `${headerLine}
`,
+ `${safeBody}
`,
+ '---
',
+ '',
+ ' ',
+ ' ',
+ '
',
+ ].join('');
+ const appendToExistingNoteHtml = `update_at: ${escapeHtml(ts)}
${safeBody}
---
`;
const pageHtml = [
'',
@@ -264,12 +773,70 @@ app.post('/api/onenote/append', async (c) => {
const sectionId = String(body?.sectionId || ONENOTE_TARGET.sectionId || '').trim();
const pageId = String(body?.pageId || ONENOTE_TARGET.pageId || '').trim();
if (!sectionId) return c.json({ success: false, message: 'No sectionId — run GET /api/onenote/site-sections first to retrieve valid IDs' }, 400);
- if (syncMode === 'edit' && !pageId) {
- return c.json({ success: false, message: 'Edit mode requires pageId' }, 400);
+ if ((syncMode === 'edit' || syncMode === 'add_to') && !pageId) {
+ return c.json({ success: false, message: 'Add/Edit mode requires pageId' }, 400);
}
- if (syncMode === 'edit' && !targetNoteId) {
- return c.json({ success: false, message: 'Edit mode requires targetNoteId or noteId' }, 400);
+ if ((syncMode === 'edit' || syncMode === 'add_to') && !targetNoteId) {
+ return c.json({ success: false, message: 'Add/Edit mode requires targetNoteId or noteId' }, 400);
}
+ if (syncMode === 'add_separate' && pageId && !targetObjectId) {
+ return c.json({ success: false, message: 'Separate mode requires a discovered OneNote target object on the selected page' }, 400);
+ }
+
+ const user = pb.authStore.model;
+ const notesWarnings: string[] = [];
+ const notesPayload = buildNotesPayload({
+ user,
+ bodyPlain,
+ title,
+ noteType,
+ jobNumber,
+ userEmail,
+ noteId: effectiveNoteId,
+ });
+
+ let notesRecord: any;
+ try {
+ notesRecord = await pb.collection(PB_NOTES_COLLECTION).create(notesPayload);
+ } catch (createErr) {
+ const createMessage = (createErr as Error)?.message || String(createErr);
+ notesWarnings.push(`Notes create retry without note_id: ${createMessage}`);
+
+ const fallbackPayload: Record = { ...notesPayload };
+ delete fallbackPayload.note_id;
+ delete fallbackPayload.onenote_id;
+
+ try {
+ notesRecord = await pb.collection(PB_NOTES_COLLECTION).create(fallbackPayload);
+ } catch (fallbackErr) {
+ const fallbackMessage = (fallbackErr as Error)?.message || String(fallbackErr);
+ return c.json(
+ { success: false, message: 'Failed to create Notes record before OneNote sync', details: { create: fallbackMessage } },
+ 500,
+ );
+ }
+ }
+
+ const notesRecordKeys = Object.keys(notesRecord || {});
+ const noteIdField = findRecordFieldName(notesRecord as Record, [/^note_?id$/i, /^noteid$/i]);
+ const oneNoteIdField = findRecordFieldName(notesRecord as Record, [/^onenote_?id$/i, /^one_?note_?id$/i, /^onenoteid$/i]);
+
+ const upsertSyncFields = async (effectiveNoteId: string): Promise => {
+ try {
+ const updatePayload: Record = {};
+ if (noteIdField) updatePayload[noteIdField] = effectiveNoteId;
+ if (oneNoteIdField) updatePayload[oneNoteIdField] = effectiveNoteId;
+
+ if (!noteIdField && !oneNoteIdField) {
+ notesWarnings.push(`No note_id/onenote_id fields found on Notes record. Fields: ${notesRecordKeys.join(', ')}`);
+ return;
+ }
+
+ await pb.collection(PB_NOTES_COLLECTION).update(notesRecord.id, updatePayload);
+ } catch (updateErr) {
+ notesWarnings.push(`Notes sync field update failed: ${(updateErr as Error)?.message || String(updateErr)}`);
+ }
+ };
const authHeader = { Authorization: `Bearer ${delegatedToken}` };
@@ -295,8 +862,19 @@ app.post('/api/onenote/append', async (c) => {
);
if (replaceResp.ok) {
+ const onenoteId = targetNoteId;
+ await upsertSyncFields(targetNoteId);
console.log('onenote_edit_success', { mode: 'edit', noteId, targetNoteId, pageId, siteId });
- return c.json({ success: true, mode: 'edited', pageId, noteId: targetNoteId, syncedAt: ts });
+ return c.json({
+ success: true,
+ mode: 'edited',
+ pageId,
+ noteId: targetNoteId,
+ onenoteId,
+ notesRecordId: notesRecord.id,
+ notesWarnings,
+ syncedAt: ts,
+ });
}
const replaceErr = await replaceResp.text();
@@ -305,7 +883,52 @@ app.post('/api/onenote/append', async (c) => {
{
success: false,
message: `OneNote edit failed (${replaceResp.status})`,
- details: { edit: replaceErr, pageId, targetNoteId, siteId },
+ details: { edit: replaceErr, pageId, targetNoteId, siteId, notesRecordId: notesRecord.id, notesWarnings },
+ },
+ 502,
+ );
+ }
+
+ if (syncMode === 'add_to') {
+ const selectorNoteId = targetNoteId.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+ const appendToNoteResp = await fetch(
+ `${base}/pages/${pageId}/content`,
+ {
+ method: 'PATCH',
+ headers: { ...authHeader, 'Content-Type': 'application/json' },
+ body: JSON.stringify([
+ {
+ target: `div[data-prism-notes-note-id=\"${selectorNoteId}\"]`,
+ action: 'append',
+ content: appendToExistingNoteHtml,
+ },
+ ]),
+ },
+ );
+
+ if (appendToNoteResp.ok) {
+ const onenoteId = targetNoteId;
+ await upsertSyncFields(targetNoteId);
+ console.log('onenote_add_to_note_success', { mode: 'add_to', noteId, targetNoteId, pageId, siteId });
+ return c.json({
+ success: true,
+ mode: 'added_to_note',
+ pageId,
+ noteId: targetNoteId,
+ onenoteId,
+ notesRecordId: notesRecord.id,
+ notesWarnings,
+ syncedAt: ts,
+ });
+ }
+
+ const appendToNoteErr = await appendToNoteResp.text();
+ console.warn('onenote_add_to_note_failed', { status: appendToNoteResp.status, error: appendToNoteErr, pageId, targetNoteId });
+ return c.json(
+ {
+ success: false,
+ message: `OneNote add-to-note failed (${appendToNoteResp.status})`,
+ details: { addToNote: appendToNoteErr, pageId, targetNoteId, siteId, notesRecordId: notesRecord.id, notesWarnings },
},
502,
);
@@ -313,18 +936,81 @@ app.post('/api/onenote/append', async (c) => {
/* ---- 1: try append to known page (if we have a valid pageId) ---- */
if (pageId) {
- const patchResp = await fetch(
- `${base}/pages/${pageId}/content`,
- {
- method: 'PATCH',
- headers: { ...authHeader, 'Content-Type': 'application/json' },
- body: JSON.stringify([{ target: 'body', action: 'append', content: appendHtml }]),
- },
- );
+ const pageAppendHtml = syncMode === 'add_separate' ? separateNoteHtml : appendHtml;
+ const action = syncMode === 'add_separate' ? 'insert' : 'append';
+
+ let patchResp: Response | null = null;
+ let targetUsed = '';
+ if (syncMode === 'add_separate') {
+ const candidates = objectTargetCandidates(targetObjectId);
+ if (!candidates.length) {
+ return c.json({ success: false, message: 'Separate mode could not build target candidates from object anchor' }, 400);
+ }
+
+ for (const candidate of candidates) {
+ for (const position of ['after', 'before'] as const) {
+ const command = {
+ target: candidate,
+ action: 'insert',
+ position,
+ content: pageAppendHtml,
+ };
+
+ const resp = await fetch(
+ `${base}/pages/${pageId}/content`,
+ {
+ method: 'PATCH',
+ headers: { ...authHeader, 'Content-Type': 'application/json' },
+ body: JSON.stringify([command]),
+ },
+ );
+ if (resp.ok) {
+ patchResp = resp;
+ targetUsed = `${candidate} (${position})`;
+ break;
+ }
+ patchResp = resp;
+ }
+ if (patchResp?.ok) {
+ break;
+ }
+ }
+ } else {
+ const command = {
+ target: 'body',
+ action,
+ content: pageAppendHtml,
+ };
+ patchResp = await fetch(
+ `${base}/pages/${pageId}/content`,
+ {
+ method: 'PATCH',
+ headers: { ...authHeader, 'Content-Type': 'application/json' },
+ body: JSON.stringify([command]),
+ },
+ );
+ }
+
+ if (!patchResp) {
+ return c.json({ success: false, message: 'OneNote patch failed before request execution' }, 502);
+ }
if (patchResp.ok) {
- console.log('onenote_append_success', { mode: 'patch', noteId, siteId });
- return c.json({ success: true, mode: 'appended', syncedAt: ts });
+ const onenoteId = effectiveNoteId;
+ await upsertSyncFields(effectiveNoteId);
+ console.log('onenote_append_success', { mode: 'patch', noteId: effectiveNoteId, siteId });
+ return c.json({
+ success: true,
+ mode: syncMode === 'add_separate' ? 'separate_inserted' : 'appended',
+ operation: action,
+ targetUsed: targetUsed || null,
+ pageId,
+ noteId: effectiveNoteId,
+ onenoteId,
+ notesRecordId: notesRecord.id,
+ notesWarnings,
+ syncedAt: ts,
+ });
}
const patchErr = await patchResp.text();
@@ -343,14 +1029,30 @@ app.post('/api/onenote/append', async (c) => {
if (postResp.ok) {
const created = await postResp.json().catch(() => ({}));
- console.log('onenote_create_success', { mode: 'create', noteId, pageId: created?.id, siteId });
- return c.json({ success: true, mode: 'created', pageId: created?.id || null, syncedAt: ts });
+ const createdPageId = created?.id ? String(created.id) : '';
+ const onenoteId = effectiveNoteId;
+ await upsertSyncFields(effectiveNoteId);
+ console.log('onenote_create_success', { mode: 'create', noteId: effectiveNoteId, pageId: created?.id, siteId });
+ return c.json({
+ success: true,
+ mode: 'created',
+ pageId: createdPageId || null,
+ noteId: effectiveNoteId,
+ onenoteId: onenoteId || null,
+ notesRecordId: notesRecord.id,
+ notesWarnings,
+ syncedAt: ts,
+ });
}
const postErr = await postResp.text();
console.error('onenote_create_failed', { status: postResp.status, postErr, siteId, sectionId });
return c.json(
- { success: false, message: `OneNote write failed (create ${postResp.status})`, details: { create: postErr, siteId, sectionId } },
+ {
+ success: false,
+ message: `OneNote write failed (create ${postResp.status})`,
+ details: { create: postErr, siteId, sectionId, notesRecordId: notesRecord.id, notesWarnings },
+ },
502,
);
} catch (e) {
@@ -364,8 +1066,6 @@ app.post('/api/onenote/append', async (c) => {
// ---------------------------------------------------------------------------
// POST /api/submit — save a note record to PocketBase Notes collection
// ---------------------------------------------------------------------------
-const PB_NOTES_COLLECTION = process.env.PB_NOTES_COLLECTION || 'Notes';
-
app.post('/api/submit', async (c) => {
try {
const body = await c.req.json();
@@ -382,29 +1082,17 @@ app.post('/api/submit', async (c) => {
const title = String(body.title || 'Untitled').trim();
const noteType = String(body.noteType || 'personal').trim();
const jobNumber = String(body.jobNumber || '').trim();
+ const noteId = String(body.noteId || '').trim();
- // Convert plain text to simple HTML (matches what the main app stores)
- const bodyHtml = bodyPlain
- .split(/\r\n|\r|\n/)
- .map((line) => `${line || ' '}
`)
- .join('');
-
- const payload: Record = {
- body_plain: bodyPlain,
- body_html: bodyHtml,
+ const payload = buildNotesPayload({
+ user,
+ bodyPlain,
title,
- type: noteType,
- email: user?.email || body.userEmail || '',
- Username: user?.name || user?.username || user?.email || '',
- userId: user?.id || '',
- job_note: noteType === 'job' || !!jobNumber,
- shared: false,
- shared_with: [],
- };
-
- if (jobNumber) {
- payload['Job_Number'] = jobNumber;
- }
+ noteType,
+ jobNumber,
+ userEmail: String(body.userEmail || '').trim(),
+ noteId,
+ });
const record = await pb.collection(PB_NOTES_COLLECTION).create(payload);
@@ -423,6 +1111,7 @@ app.post('/api/submit', async (c) => {
// ---------------------------------------------------------------------------
app.use('/images/*', serveStatic({ root: new URL('./', import.meta.url).pathname }));
app.use('/tools/*', serveStatic({ root: new URL('./', import.meta.url).pathname }));
+app.use('/legal/*', serveStatic({ root: new URL('./', import.meta.url).pathname }));
app.get('/vendor/msal-browser.js', async (c) => {
const file = Bun.file(new URL('./node_modules/@azure/msal-browser/lib/msal-browser.min.js', import.meta.url));
@@ -436,6 +1125,53 @@ app.get('/', async (c) => {
return c.html(html);
});
+app.get('/notes', async (c) => {
+ try {
+ const html = await loadLegacyNotesHtml();
+ return c.html(html);
+ } catch (error) {
+ console.error('Failed to load legacy notes UI', error);
+ return c.html(`
+
+
+
+
+ Prism Notes Legacy
+
+
+
+
+
+ Prism Notes
+ Legacy notes UI unavailable
+ The current server could not load the historical notes interface from git commit ${LEGACY_NOTES_COMMIT}.
+ ${escapeHtml(String(error instanceof Error ? error.message : error || 'Unknown error'))}
+
+
+
+
+`, 500);
+ }
+});
+
+app.get('/tasks', async (c) => {
+ const html = await Bun.file(new URL('./tasks.html', import.meta.url)).text();
+ return c.html(html);
+});
+
+app.get('/legal/privacy', async (c) => {
+ const html = await Bun.file(new URL('./legal/privacy.html', import.meta.url)).text();
+ return c.html(html);
+});
+
+app.get('/legal/eula', async (c) => {
+ const html = await Bun.file(new URL('./legal/eula.html', import.meta.url)).text();
+ return c.html(html);
+});
+
// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------
diff --git a/tasks.html b/tasks.html
new file mode 100644
index 0000000..74921e3
--- /dev/null
+++ b/tasks.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+ Prism Notes Tasks
+
+
+
+
+
+
+
+
+
+
+
+ Prism Notes
+ Tasks View
+
+
+
+
+ Browse PocketBase Tasgird tasks. Default filter is current user, with optional user switching.
+
+
+
+
+ Server
+ Checking...
+
+
+ PocketBase User
+ Not logged in
+
+
+ Task Count
+ 0
+
+
+
+
+
+ 1) PocketBase Login
+ Uses your existing PocketBase Microsoft OAuth provider.
+
+
+
+
+
+
+
+ 2) Task Filter
+ Defaults to your tasks, but you can select other users.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tasks (Tasgird)
+
+
+
+
+ id
+ user
+ title
+ startDate (UTC)
+ dueDate (UTC)
+ priority
+ completed
+ content
+ size
+ status
+
+
+
+ No tasks loaded.
+
+
+
+
+
+
+ Output
+
+
+
+
+
+
+