Add isolated legacy notes route and tasks view with resilient task APIs

This commit is contained in:
2026-03-28 19:17:56 +00:00
parent 8ca2c97e37
commit d6b0e3192a
5 changed files with 1516 additions and 66 deletions
+171 -18
View File
@@ -93,11 +93,17 @@
<body class="min-h-screen bg-gradient-to-b from-slate-950 via-slate-950 to-slate-900 text-slate-100">
<main class="mx-auto max-w-6xl px-6 py-10">
<header class="panel mb-8">
<div class="flex items-center gap-4">
<img src="/images/prism.png" alt="Prism Notes" class="h-14 w-14 rounded-xl object-cover ring-1 ring-slate-600/60" />
<div>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-400">Prism Notes</p>
<h1 class="mt-1 text-3xl font-semibold tracking-tight">Fresh rebuild</h1>
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-4">
<img src="/images/prism.png" alt="Prism Notes" class="h-14 w-14 rounded-xl object-cover ring-1 ring-slate-600/60" />
<div>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-400">Prism Notes</p>
<h1 class="mt-1 text-3xl font-semibold tracking-tight">Fresh rebuild</h1>
</div>
</div>
<div class="flex flex-wrap gap-3">
<a href="/notes" class="btn btn-secondary">Open Legacy Notes</a>
<a href="/tasks" class="btn btn-secondary">Open Tasks View</a>
</div>
</div>
<p class="mt-3 text-sm text-slate-300">Clean baseline UI with PocketBase auth, OneNote delegated auth, note submit, and OneNote proof actions.</p>
@@ -167,13 +173,14 @@
<div>
<label class="text-sm font-medium text-slate-300">Sync Mode</label>
<select id="syncModeSelect" class="field mt-1">
<option value="add">add new note block</option>
<option value="edit">edit existing note block</option>
<option value="add_to" selected>add to same box (default)</option>
<option value="add_separate">add as separate box</option>
<option value="edit">edit note</option>
</select>
</div>
<div>
<label class="text-sm font-medium text-slate-300">Existing Note ID (edit mode)</label>
<input id="syncEditNoteId" class="field mt-1" placeholder="Paste noteId to replace on selected page" />
<label class="text-sm font-medium text-slate-300">Existing Note ID (add to/edit)</label>
<input id="syncEditNoteId" class="field mt-1" placeholder="Pick from page list or paste noteId" />
</div>
</div>
<div class="flex flex-wrap items-center gap-3">
@@ -188,6 +195,14 @@
<label class="text-sm font-medium text-slate-300">OneNote Page (optional)</label>
<select id="syncPageSelect" class="field mt-1"><option value="">— create new page in selected section —</option></select>
</div>
<div>
<label class="text-sm font-medium text-slate-300">Separate Insert Anchor (optional)</label>
<input id="syncObjectAnchor" class="field mt-1" placeholder="Paste OneNote paragraph/object link or object-id token" />
</div>
<div>
<label class="text-sm font-medium text-slate-300">Notes on Selected Page</label>
<select id="syncExistingNoteSelect" class="field mt-1"><option value="">— select a page first —</option></select>
</div>
<div>
<label class="text-sm font-medium text-slate-300">New Page Title (used when page is not selected)</label>
<input id="syncPageTitle" class="field mt-1" placeholder="Optional (defaults to note title)" />
@@ -253,8 +268,11 @@
const syncEditNoteId = document.getElementById('syncEditNoteId');
const syncSectionSelect = document.getElementById('syncSectionSelect');
const syncPageSelect = document.getElementById('syncPageSelect');
const syncObjectAnchor = document.getElementById('syncObjectAnchor');
const syncExistingNoteSelect = document.getElementById('syncExistingNoteSelect');
const syncPageTitle = document.getElementById('syncPageTitle');
let resolvedSiteId = null;
let syncPageNoteBlocks = [];
const noteTitle = document.getElementById('noteTitle');
const noteJobNumber = document.getElementById('noteJobNumber');
@@ -514,6 +532,46 @@
};
}
function isExistingNoteMode() {
const mode = String(syncModeSelect.value || '').trim();
return mode === 'add_to' || mode === 'edit';
}
function parseObjectAnchor(rawValue) {
const raw = String(rawValue || '').trim();
if (!raw) return '';
try {
const decoded = decodeURIComponent(raw);
const m = decoded.match(/object-id=\{([0-9A-Fa-f-]{36})\}&([0-9A-Fa-f]+)/i);
if (m) {
return `{${m[1]}}&${m[2]}`;
}
} catch {
}
const direct = raw.match(/\{([0-9A-Fa-f-]{36})\}(?:&([0-9A-Fa-f]+))?/);
if (direct) {
return direct[2] ? `{${direct[1]}}&${direct[2]}` : `{${direct[1]}}`;
}
return raw;
}
function getSelectedPageNote() {
const noteId = syncExistingNoteSelect.value.trim();
return syncPageNoteBlocks.find((block) => block.noteId === noteId) || null;
}
function applySyncModeUi() {
const needsExisting = isExistingNoteMode();
syncEditNoteId.disabled = !needsExisting;
syncExistingNoteSelect.disabled = !needsExisting;
if (!needsExisting) {
syncEditNoteId.value = '';
}
}
async function loadSyncTargets() {
syncTargetsStatus.textContent = 'Loading sections…';
if (!hasUsableToken()) {
@@ -560,6 +618,8 @@
async function loadSyncPagesForSelectedSection() {
const sectionId = syncSectionSelect.value.trim();
syncPageSelect.innerHTML = '<option value="">— create new page in selected section —</option>';
syncExistingNoteSelect.innerHTML = '<option value="">— select a page first —</option>';
syncPageNoteBlocks = [];
if (!sectionId) {
syncTargetsStatus.textContent = 'Select a section first';
@@ -589,6 +649,58 @@
writeOutput({ message: `Loaded ${pages.length} page(s)`, sectionId, pages });
}
async function loadPageNoteBlocksForSelectedPage() {
const pageId = syncPageSelect.value.trim();
syncPageNoteBlocks = [];
syncExistingNoteSelect.innerHTML = '';
if (!pageId) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = '— select a page first —';
syncExistingNoteSelect.appendChild(opt);
return;
}
syncTargetsStatus.textContent = 'Loading page note blocks…';
const resp = await fetch(`/api/onenote/page-note-blocks?pageId=${encodeURIComponent(pageId)}`, {
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);
}
syncPageNoteBlocks = Array.isArray(data.noteBlocks) ? data.noteBlocks : [];
if (!syncPageNoteBlocks.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = '— no notes found on this page —';
syncExistingNoteSelect.appendChild(opt);
syncTargetsStatus.textContent = '✓ 0 notes found';
return;
}
for (const block of syncPageNoteBlocks) {
const opt = document.createElement('option');
opt.value = block.noteId;
const preview = String(block.preview || '').replace(/\s+/g, ' ').trim().slice(0, 70);
const syncedLabel = block.syncedAt ? ` @ ${block.syncedAt}` : '';
opt.textContent = `${block.noteId}${syncedLabel}${preview || '(empty)'}`;
syncExistingNoteSelect.appendChild(opt);
}
syncExistingNoteSelect.value = syncPageNoteBlocks[0].noteId || '';
if (isExistingNoteMode()) {
syncEditNoteId.value = syncExistingNoteSelect.value;
}
syncTargetsStatus.textContent = `${syncPageNoteBlocks.length} note(s) loaded`;
writeOutput({ message: `Loaded ${syncPageNoteBlocks.length} note(s)`, pageId, notes: syncPageNoteBlocks });
}
async function submitNoteToPrismNotes() {
const client = await ensurePocketBaseClient();
if (!client.authStore.isValid || !client.authStore.token) {
@@ -619,23 +731,36 @@
throw new Error('OneNote connection required');
}
const syncMode = syncModeSelect.value === 'edit' ? 'edit' : 'add';
const requestedMode = String(syncModeSelect.value || '').trim();
const syncMode = requestedMode === 'add_to' ? 'add_to' : requestedMode === 'edit' ? 'edit' : 'add_separate';
const sectionId = syncSectionSelect.value.trim();
const pageId = syncPageSelect.value.trim();
const pageTitle = syncPageTitle.value.trim();
const targetNoteId = syncEditNoteId.value.trim();
const selectedNoteId = syncExistingNoteSelect.value.trim();
const selectedPageNote = getSelectedPageNote();
const manualAnchor = parseObjectAnchor(syncObjectAnchor.value);
const targetNoteId = syncMode === 'add_to' || syncMode === 'edit'
? (syncEditNoteId.value.trim() || selectedNoteId)
: '';
const targetObjectId = syncMode === 'add_separate'
? (manualAnchor || syncPageNoteBlocks.at(-1)?.targetId || '')
: (selectedPageNote?.targetId || '');
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 === 'add_to' || syncMode === 'edit') && !pageId) {
throw new Error('Add/Edit mode requires selecting an existing OneNote page');
}
if (syncMode === 'edit' && !targetNoteId) {
throw new Error('Edit mode requires Existing Note ID');
if ((syncMode === 'add_to' || syncMode === 'edit') && !targetNoteId) {
throw new Error('Add/Edit mode requires Existing Note ID');
}
const payload = buildNotePayload({ noteId: syncMode === 'edit' ? targetNoteId : '' });
if (syncMode === 'add_to' || syncMode === 'edit') {
syncEditNoteId.value = targetNoteId;
}
const payload = buildNotePayload({ noteId: syncMode === 'add_separate' ? '' : targetNoteId });
const resp = await fetch('/api/onenote/append', {
method: 'POST',
headers: {
@@ -650,6 +775,7 @@
pageId,
pageTitle,
targetNoteId,
targetObjectId,
}),
});
const data = await resp.json().catch(() => ({}));
@@ -657,14 +783,19 @@
throw new Error(data?.message || 'OneNote sync failed');
}
if (syncMode === 'add' && data?.pageId && !pageId) {
if (syncMode === 'add_separate' && 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;
await loadPageNoteBlocksForSelectedPage();
} else if (pageId) {
await loadPageNoteBlocksForSelectedPage();
}
if (syncMode === 'add_to' || syncMode === 'edit') {
syncEditNoteId.value = payload.noteId;
}
syncEditNoteId.value = payload.noteId;
writeOutput({ message: `OneNote sync complete (${data?.mode || syncMode})`, response: data, payload });
}
@@ -730,10 +861,32 @@
}
});
syncPageSelect.addEventListener('change', async () => {
try {
await loadPageNoteBlocksForSelectedPage();
} catch (error) {
writeOutput({ error: error?.message || String(error) });
}
});
syncExistingNoteSelect.addEventListener('change', () => {
if (isExistingNoteMode()) {
syncEditNoteId.value = syncExistingNoteSelect.value.trim();
}
});
syncModeSelect.addEventListener('change', () => {
applySyncModeUi();
if (isExistingNoteMode() && syncExistingNoteSelect.value.trim()) {
syncEditNoteId.value = syncExistingNoteSelect.value.trim();
}
});
ensurePocketBaseClient()
.then(() => updatePbStatus())
.catch((error) => writeOutput({ error: error?.message || String(error) }));
updateOneNoteStatus();
applySyncModeUi();
loadHealth();
loadGraphStatus();
handleMsalRedirect().catch((error) => writeOutput({ error: error?.message || String(error) }));