Upgrade OneNote sync with section/page targeting and edit mode

- Add GET /api/onenote/section-pages endpoint for listing pages in a section
- Extend /api/onenote/append with syncMode add/edit support
- Support editing an existing note block by noteId on a selected page
- Keep add mode for appending to selected page or creating new page in section
- Add Note + Sync UI controls for sync mode, section/page target, existing noteId,
  and optional new page title
- Wire frontend flow to load sync targets and pages, then sync with selected mode
This commit is contained in:
2026-03-24 18:45:27 +00:00
parent 513e975530
commit 8ca2c97e37
2 changed files with 236 additions and 39 deletions
+75 -4
View File
@@ -173,15 +173,43 @@ app.get('/api/onenote/site-sections', async (c) => {
}
});
// GET /api/onenote/section-pages?sectionId=...
// Requires: Authorization: Bearer <delegated token>
app.get('/api/onenote/section-pages', async (c) => {
try {
const delegatedToken = getBearerToken(c.req.header('authorization'));
if (!delegatedToken) return c.json({ success: false, message: 'Missing delegated token' }, 401);
const sectionId = String(c.req.query('sectionId') || '').trim();
if (!sectionId) return c.json({ success: false, message: 'Missing sectionId' }, 400);
const siteId = await resolveSiteId(delegatedToken);
const resp = await fetch(
`https://graph.microsoft.com/v1.0/sites/${siteId}/onenote/sections/${encodeURIComponent(sectionId)}/pages?$top=100&$select=id,title,createdDateTime,lastModifiedDateTime`,
{ headers: { Authorization: `Bearer ${delegatedToken}` } },
);
const data = await resp.json().catch(() => ({}));
if (!resp.ok) return c.json({ success: false, siteId, sectionId, error: data }, resp.status as any);
return c.json({ success: true, siteId, sectionId, pages: data?.value || [] });
} 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 <delegated Graph token>
// Body: { noteId, title, bodyPlain, noteType?, userEmail?, jobNumber?, sectionId? }
// Body: {
// noteId, title, bodyPlain, noteType?, userEmail?, jobNumber?,
// sectionId?, pageId?, pageTitle?, syncMode?: 'add'|'edit', targetNoteId?
// }
//
// Strategy:
// 1. Resolve site ID via GET /sites/{host}:{path}
// 2. Try PATCH sites/{siteId}/onenote/pages/{pageId}/content → append to known page
// 3. If that fails, POST sites/{siteId}/onenote/sections/{sectionId}/pages → create new page
// 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
// ---------------------------------------------------------------------------
app.post('/api/onenote/append', async (c) => {
try {
@@ -206,6 +234,9 @@ app.post('/api/onenote/append', async (c) => {
const noteType = String(body?.noteType || 'personal').trim();
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 targetNoteId = String(body?.targetNoteId || noteId || '').trim();
if (!noteId) return c.json({ success: false, message: 'Missing noteId' }, 400);
if (!bodyPlain) return c.json({ success: false, message: 'Missing bodyPlain' }, 400);
@@ -225,7 +256,7 @@ app.post('/api/onenote/append', async (c) => {
const pageHtml = [
'<!DOCTYPE html>',
'<html>',
`<head><title>${escapeHtml(title)}</title></head>`,
`<head><title>${escapeHtml(pageTitle)}</title></head>`,
`<body>${appendHtml}</body>`,
'</html>',
].join('');
@@ -233,6 +264,12 @@ 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' && !targetNoteId) {
return c.json({ success: false, message: 'Edit mode requires targetNoteId or noteId' }, 400);
}
const authHeader = { Authorization: `Bearer ${delegatedToken}` };
@@ -240,6 +277,40 @@ app.post('/api/onenote/append', async (c) => {
const siteId = await resolveSiteId(delegatedToken);
const base = `https://graph.microsoft.com/v1.0/sites/${siteId}/onenote`;
if (syncMode === 'edit') {
const selectorNoteId = targetNoteId.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const replaceResp = 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: 'replace',
content: appendHtml,
},
]),
},
);
if (replaceResp.ok) {
console.log('onenote_edit_success', { mode: 'edit', noteId, targetNoteId, pageId, siteId });
return c.json({ success: true, mode: 'edited', pageId, noteId: targetNoteId, syncedAt: ts });
}
const replaceErr = await replaceResp.text();
console.warn('onenote_edit_failed', { status: replaceResp.status, error: replaceErr, pageId, targetNoteId });
return c.json(
{
success: false,
message: `OneNote edit failed (${replaceResp.status})`,
details: { edit: replaceErr, pageId, targetNoteId, siteId },
},
502,
);
}
/* ---- 1: try append to known page (if we have a valid pageId) ---- */
if (pageId) {
const patchResp = await fetch(