diff --git a/index.html b/index.html
index 1a77321..9c64121 100644
--- a/index.html
+++ b/index.html
@@ -163,6 +163,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -219,6 +248,12 @@
const oneNoteAuthStatus = document.getElementById('oneNoteAuthStatus');
const resolveStatus = document.getElementById('resolveStatus');
const sectionSelect = document.getElementById('sectionSelect');
+ const syncTargetsStatus = document.getElementById('syncTargetsStatus');
+ const syncModeSelect = document.getElementById('syncModeSelect');
+ const syncEditNoteId = document.getElementById('syncEditNoteId');
+ const syncSectionSelect = document.getElementById('syncSectionSelect');
+ const syncPageSelect = document.getElementById('syncPageSelect');
+ const syncPageTitle = document.getElementById('syncPageTitle');
let resolvedSiteId = null;
const noteTitle = document.getElementById('noteTitle');
@@ -230,6 +265,7 @@
const proofBody = document.getElementById('proofBody');
proofTitle.value = `Prism Notes Proof ${new Date().toISOString()}`;
+ syncPageTitle.value = `Prism Notes Sync ${new Date().toISOString().slice(0, 10)}`;
proofBody.value = [
'Proof that OneNote can be updated programmatically from outside OneNote.',
`Created: ${new Date().toISOString()}`,
@@ -461,14 +497,15 @@
writeOutput({ message: 'PocketBase session cleared.' });
}
- function buildNotePayload() {
+ function buildNotePayload(options = {}) {
const bodyPlain = noteBody.value.trim();
if (!bodyPlain) {
throw new Error('Note body is required');
}
const title = noteTitle.value.trim() || 'Untitled';
+ const requestedNoteId = String(options.noteId || '').trim();
return {
- noteId: globalThis.crypto?.randomUUID?.() || `note-${Date.now()}`,
+ noteId: requestedNoteId || globalThis.crypto?.randomUUID?.() || `note-${Date.now()}`,
title,
bodyPlain,
noteType: noteType.value || 'personal',
@@ -477,6 +514,81 @@
};
}
+ async function loadSyncTargets() {
+ syncTargetsStatus.textContent = 'Loading sections…';
+ if (!hasUsableToken()) {
+ syncTargetsStatus.textContent = '⚠ Connect OneNote first';
+ throw new Error('OneNote connection required');
+ }
+
+ const resp = await fetch('/api/onenote/site-sections', {
+ headers: { Authorization: `Bearer ${delegatedAccessToken}` },
+ });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok || !data?.success) {
+ const msg = data?.message || JSON.stringify(data?.error || data);
+ syncTargetsStatus.textContent = `⚠ ${msg}`;
+ throw new Error(msg);
+ }
+
+ resolvedSiteId = data.siteId;
+ const sections = Array.isArray(data.sections) ? data.sections : [];
+
+ syncSectionSelect.innerHTML = '';
+ sectionSelect.innerHTML = '';
+ for (const section of sections) {
+ const label = `${section.displayName || section.id} — ${section.id}`;
+
+ const syncOpt = document.createElement('option');
+ syncOpt.value = section.id;
+ syncOpt.textContent = label;
+ syncSectionSelect.appendChild(syncOpt);
+
+ const proofOpt = document.createElement('option');
+ proofOpt.value = section.id;
+ proofOpt.textContent = label;
+ sectionSelect.appendChild(proofOpt);
+ }
+
+ syncTargetsStatus.textContent = `✓ ${sections.length} section(s) loaded`;
+ resolveStatus.textContent = `✓ ${sections.length} section(s) loaded`;
+
+ await loadSyncPagesForSelectedSection();
+ writeOutput({ message: `Loaded ${sections.length} section(s)`, siteId: resolvedSiteId, sections });
+ }
+
+ async function loadSyncPagesForSelectedSection() {
+ const sectionId = syncSectionSelect.value.trim();
+ syncPageSelect.innerHTML = '';
+
+ if (!sectionId) {
+ syncTargetsStatus.textContent = 'Select a section first';
+ return;
+ }
+
+ syncTargetsStatus.textContent = 'Loading pages…';
+ const resp = await fetch(`/api/onenote/section-pages?sectionId=${encodeURIComponent(sectionId)}`, {
+ headers: { Authorization: `Bearer ${delegatedAccessToken}` },
+ });
+ const data = await resp.json().catch(() => ({}));
+ if (!resp.ok || !data?.success) {
+ const msg = data?.message || JSON.stringify(data?.error || data);
+ syncTargetsStatus.textContent = `⚠ ${msg}`;
+ throw new Error(msg);
+ }
+
+ const pages = Array.isArray(data.pages) ? data.pages : [];
+ for (const page of pages) {
+ const opt = document.createElement('option');
+ opt.value = page.id;
+ opt.textContent = `${page.title || '(untitled)'} — ${page.id}`;
+ syncPageSelect.appendChild(opt);
+ }
+
+ syncTargetsStatus.textContent = `✓ ${pages.length} page(s) loaded`;
+ writeOutput({ message: `Loaded ${pages.length} page(s)`, sectionId, pages });
+ }
+
async function submitNoteToPrismNotes() {
const client = await ensurePocketBaseClient();
if (!client.authStore.isValid || !client.authStore.token) {
@@ -507,7 +619,23 @@
throw new Error('OneNote connection required');
}
- const payload = buildNotePayload();
+ const syncMode = syncModeSelect.value === 'edit' ? 'edit' : 'add';
+ const sectionId = syncSectionSelect.value.trim();
+ const pageId = syncPageSelect.value.trim();
+ const pageTitle = syncPageTitle.value.trim();
+ const targetNoteId = syncEditNoteId.value.trim();
+
+ if (!sectionId) {
+ throw new Error('Load sync targets and select a section first');
+ }
+ if (syncMode === 'edit' && !pageId) {
+ throw new Error('Edit mode requires selecting an existing OneNote page');
+ }
+ if (syncMode === 'edit' && !targetNoteId) {
+ throw new Error('Edit mode requires Existing Note ID');
+ }
+
+ const payload = buildNotePayload({ noteId: syncMode === 'edit' ? targetNoteId : '' });
const resp = await fetch('/api/onenote/append', {
method: 'POST',
headers: {
@@ -515,46 +643,35 @@
Authorization: `Bearer ${delegatedAccessToken}`,
'x-pb-token': client.authStore.token,
},
- body: JSON.stringify(payload),
+ body: JSON.stringify({
+ ...payload,
+ syncMode,
+ sectionId,
+ pageId,
+ pageTitle,
+ targetNoteId,
+ }),
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success) {
throw new Error(data?.message || 'OneNote sync failed');
}
- writeOutput({ message: 'Synced to configured OneNote target', response: data, payload });
+
+ if (syncMode === 'add' && data?.pageId && !pageId) {
+ const createdOpt = document.createElement('option');
+ createdOpt.value = data.pageId;
+ createdOpt.textContent = `${pageTitle || payload.title} — ${data.pageId}`;
+ syncPageSelect.appendChild(createdOpt);
+ syncPageSelect.value = data.pageId;
+ }
+ syncEditNoteId.value = payload.noteId;
+
+ writeOutput({ message: `OneNote sync complete (${data?.mode || syncMode})`, response: data, payload });
}
async function resolveSiteSections() {
resolveStatus.textContent = 'Resolving…';
- if (!hasUsableToken()) {
- resolveStatus.textContent = '⚠ Connect OneNote first (section 2)';
- throw new Error('OneNote connection required — click Connect OneNote in section 2 first');
- }
- try {
- const resp = await fetch('/api/onenote/site-sections', {
- headers: { Authorization: `Bearer ${delegatedAccessToken}` },
- });
- const data = await resp.json().catch(() => ({}));
- if (!resp.ok || !data?.success) {
- const msg = data?.message || JSON.stringify(data?.error || data);
- resolveStatus.textContent = `⚠ ${msg}`;
- throw new Error(msg);
- }
- resolvedSiteId = data.siteId;
- const sections = Array.isArray(data.sections) ? data.sections : [];
- sectionSelect.innerHTML = '';
- for (const s of sections) {
- const opt = document.createElement('option');
- opt.value = s.id;
- opt.textContent = `${s.displayName || s.id} — ${s.id}`;
- sectionSelect.appendChild(opt);
- }
- resolveStatus.textContent = `✓ ${sections.length} section(s) loaded`;
- writeOutput({ message: `Resolved ${sections.length} section(s)`, siteId: resolvedSiteId, sections });
- } catch (e) {
- if (!resolveStatus.textContent.startsWith('⚠')) resolveStatus.textContent = `⚠ ${e.message}`;
- throw e;
- }
+ await loadSyncTargets();
}
async function createProofPage() {
@@ -599,11 +716,20 @@
bind('logoutBtn', logoutPocketBase);
bind('connectOneNoteBtn', connectOneNoteInteractive);
bind('refreshOneNoteTokenBtn', refreshOneNoteToken);
- bind('submitNoteBtn', submitNoteToPrismNotes);
+ bind('loadSyncTargetsBtn', loadSyncTargets);
+ bind('submitNoteBtn', submitNoteToPrismNotes);
bind('syncOneNoteBtn', syncCurrentNoteToOneNote);
bind('resolveSectionsBtn', resolveSiteSections);
bind('createProofPageBtn', createProofPage);
+ syncSectionSelect.addEventListener('change', async () => {
+ try {
+ await loadSyncPagesForSelectedSection();
+ } catch (error) {
+ writeOutput({ error: error?.message || String(error) });
+ }
+ });
+
ensurePocketBaseClient()
.then(() => updatePbStatus())
.catch((error) => writeOutput({ error: error?.message || String(error) }));
diff --git a/server.ts b/server.ts
index b15d688..9b6c511 100644
--- a/server.ts
+++ b/server.ts
@@ -173,15 +173,43 @@ app.get('/api/onenote/site-sections', async (c) => {
}
});
+// GET /api/onenote/section-pages?sectionId=...
+// Requires: Authorization: Bearer
+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
-// 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 = [
'',
'',
- `${escapeHtml(title)}`,
+ `${escapeHtml(pageTitle)}`,
`${appendHtml}`,
'',
].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(