513e975530
- Replace popup OAuth with SDK authWithOAuth2 realtime channel (urlCallback) so login works in Edge app mode without navigating the main window - Remove manual redirect/sessionStorage OAuth flow that broke in Edge app mode - Serve PocketBase config (URL, collection, provider) from server endpoint backed by env vars instead of hardcoded localhost - Fix /api/submit: target Notes collection with correct field mapping (body_plain, body_html, title, type, email, Username, userId, job_note, Job_Number) instead of Job_Info_Prod with wrong field names - Remove localhost fallbacks from auth-module backend and frontend
617 lines
23 KiB
HTML
617 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Prism Notes</title>
|
|
<link rel="icon" type="image/png" href="/images/prism.png" />
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="/vendor/msal-browser.js"></script>
|
|
<style>
|
|
.panel {
|
|
border-radius: 1rem;
|
|
border: 1px solid rgb(30 41 59 / 1);
|
|
background: rgb(15 23 42 / 0.78);
|
|
backdrop-filter: blur(8px);
|
|
padding: 1.25rem;
|
|
}
|
|
.btn {
|
|
border-radius: 0.75rem;
|
|
padding: 0.55rem 1rem;
|
|
font-weight: 600;
|
|
transition: all 160ms ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.btn:focus-visible,
|
|
.field:focus-visible {
|
|
outline: 2px solid rgb(34 211 238 / 0.8);
|
|
outline-offset: 1px;
|
|
}
|
|
.btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(120deg, transparent 0%, rgb(255 255 255 / 0.2) 48%, transparent 100%);
|
|
transform: translateX(-130%);
|
|
transition: transform 260ms ease;
|
|
pointer-events: none;
|
|
}
|
|
.btn:hover {
|
|
transform: translateY(-1px) scale(1.01);
|
|
box-shadow: 0 10px 22px rgb(15 23 42 / 0.28);
|
|
}
|
|
.btn:hover::before { transform: translateX(130%); }
|
|
.btn-accent-cyan {
|
|
color: rgb(8 47 73 / 1);
|
|
background: rgb(6 182 212 / 1);
|
|
}
|
|
.btn-accent-emerald {
|
|
color: rgb(6 44 32 / 1);
|
|
background: rgb(34 197 94 / 1);
|
|
}
|
|
.btn-accent-indigo {
|
|
color: rgb(255 255 255 / 0.98);
|
|
background: rgb(99 102 241 / 1);
|
|
}
|
|
.btn-accent-violet {
|
|
color: rgb(255 255 255 / 0.98);
|
|
background: rgb(139 92 246 / 1);
|
|
}
|
|
.btn-secondary {
|
|
border: 1px solid rgb(51 65 85 / 1);
|
|
background: rgb(15 23 42 / 1);
|
|
color: rgb(226 232 240 / 1);
|
|
}
|
|
.btn-secondary:hover { border-color: rgb(100 116 139 / 1); }
|
|
.field {
|
|
width: 100%;
|
|
border-radius: 0.75rem;
|
|
border: 1px solid rgb(51 65 85 / 1);
|
|
background: rgb(2 6 23 / 1);
|
|
padding: 0.55rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
color: rgb(248 250 252 / 1);
|
|
}
|
|
.field option {
|
|
background: rgb(15 23 42 / 0.96);
|
|
color: rgb(248 250 252 / 1);
|
|
}
|
|
.field::placeholder { color: rgb(100 116 139 / 1); }
|
|
#output {
|
|
border: 1px solid rgb(30 41 59 / 1);
|
|
background: rgb(2 6 23 / 1);
|
|
}
|
|
code {
|
|
border: 1px solid rgb(51 65 85 / 1);
|
|
background: rgb(15 23 42 / 1);
|
|
padding: 0.12rem 0.35rem;
|
|
border-radius: 0.45rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<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>
|
|
</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>
|
|
</header>
|
|
|
|
<section class="mb-6 grid gap-4 md:grid-cols-3">
|
|
<article class="panel">
|
|
<p class="text-xs uppercase tracking-wider text-slate-400">Server</p>
|
|
<p id="healthStatus" class="mt-2 text-sm font-medium">Checking...</p>
|
|
</article>
|
|
<article class="panel">
|
|
<p class="text-xs uppercase tracking-wider text-slate-400">Graph App Token</p>
|
|
<p id="graphStatus" class="mt-2 text-sm font-medium">Checking...</p>
|
|
</article>
|
|
<article class="panel">
|
|
<p class="text-xs uppercase tracking-wider text-slate-400">PocketBase User</p>
|
|
<p id="pbStatus" class="mt-2 text-sm font-medium">Not logged in</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="mb-6 grid gap-6 lg:grid-cols-2">
|
|
<article class="panel p-6">
|
|
<h2 class="text-xl font-medium">1) Prism Notes Login</h2>
|
|
<p class="mt-2 text-sm text-slate-300">Uses PocketBase OAuth with Microsoft to validate the Prism Notes user session.</p>
|
|
<div class="mt-4 flex flex-wrap gap-3">
|
|
<button id="loginBtn" class="btn btn-accent-cyan">Login with Microsoft</button>
|
|
<button id="logoutBtn" class="btn btn-secondary">Logout</button>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="panel p-6">
|
|
<h2 class="text-xl font-medium">2) OneNote Connect</h2>
|
|
<p class="mt-2 text-sm text-slate-300">Gets delegated Graph token used for OneNote APIs.</p>
|
|
<p id="oneNoteAuthStatus" class="mt-3 text-sm font-medium">Disconnected</p>
|
|
<div class="mt-4 flex flex-wrap gap-3">
|
|
<button id="connectOneNoteBtn" class="btn btn-accent-emerald">Connect OneNote</button>
|
|
<button id="refreshOneNoteTokenBtn" class="btn btn-secondary">Refresh token</button>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="mb-6 grid gap-6 lg:grid-cols-2">
|
|
<article class="panel p-6">
|
|
<h2 class="text-xl font-medium">3) Note + Sync</h2>
|
|
<div class="mt-4 space-y-4">
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Title</label>
|
|
<input id="noteTitle" class="field mt-1" placeholder="Example: Billing update" />
|
|
</div>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Job Number</label>
|
|
<input id="noteJobNumber" class="field mt-1" placeholder="Optional" />
|
|
</div>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Type</label>
|
|
<select id="noteType" class="field mt-1">
|
|
<option value="personal">personal</option>
|
|
<option value="job">job</option>
|
|
<option value="billing">billing</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Body</label>
|
|
<textarea id="noteBody" rows="8" class="field mt-1" placeholder="Write note content..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex flex-wrap gap-3">
|
|
<button id="submitNoteBtn" class="btn btn-accent-indigo">Submit to Prism Notes</button>
|
|
<button id="syncOneNoteBtn" class="btn btn-accent-violet">Sync to OneNote</button>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="panel p-6">
|
|
<h2 class="text-xl font-medium">4) OneNote Proof (Direct Graph)</h2>
|
|
<p class="mt-2 text-sm text-slate-300">Creates a page via site-scoped endpoints — bypasses the <code>me/onenote</code> 5,000-item tenant limit.</p>
|
|
<div class="mt-4 space-y-4">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<button id="resolveSectionsBtn" class="btn btn-secondary">Resolve site sections</button>
|
|
<span id="resolveStatus" class="text-xs text-slate-400"></span>
|
|
</div>
|
|
<p class="text-xs text-slate-500">Fetches sections from <code>sites/czflex.sharepoint.com:/sites/Team/onenote/sections</code> — site-scoped only, no tenant scan.</p>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Section</label>
|
|
<select id="sectionSelect" class="field mt-1"><option value="">— click Resolve —</option></select>
|
|
</div>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Proof Page Title</label>
|
|
<input id="proofTitle" class="field mt-1" />
|
|
</div>
|
|
<div>
|
|
<label class="text-sm font-medium text-slate-300">Proof Message</label>
|
|
<textarea id="proofBody" rows="6" class="field mt-1"></textarea>
|
|
</div>
|
|
<button id="createProofPageBtn" class="btn btn-accent-emerald">Create OneNote Proof Page</button>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="panel p-6">
|
|
<h2 class="text-xl font-medium">Output</h2>
|
|
<pre id="output" class="mt-4 min-h-[220px] overflow-auto rounded-xl border border-slate-800 bg-slate-950 p-4 text-xs leading-5 text-slate-200"></pre>
|
|
</section>
|
|
</main>
|
|
|
|
<script type="module">
|
|
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/+esm';
|
|
|
|
const PB_DEFAULT_AUTH_COLLECTION = 'Users';
|
|
const PB_DEFAULT_OAUTH_PROVIDER = 'microsoft';
|
|
const SCOPES = ['User.Read', 'Notes.ReadWrite.All', 'Sites.Read.All', 'offline_access'];
|
|
|
|
let pb = null;
|
|
let pbConfigPromise = null;
|
|
|
|
const output = document.getElementById('output');
|
|
const healthStatus = document.getElementById('healthStatus');
|
|
const graphStatus = document.getElementById('graphStatus');
|
|
const pbStatus = document.getElementById('pbStatus');
|
|
const oneNoteAuthStatus = document.getElementById('oneNoteAuthStatus');
|
|
const resolveStatus = document.getElementById('resolveStatus');
|
|
const sectionSelect = document.getElementById('sectionSelect');
|
|
let resolvedSiteId = null;
|
|
|
|
const noteTitle = document.getElementById('noteTitle');
|
|
const noteJobNumber = document.getElementById('noteJobNumber');
|
|
const noteType = document.getElementById('noteType');
|
|
const noteBody = document.getElementById('noteBody');
|
|
|
|
const proofTitle = document.getElementById('proofTitle');
|
|
const proofBody = document.getElementById('proofBody');
|
|
|
|
proofTitle.value = `Prism Notes Proof ${new Date().toISOString()}`;
|
|
proofBody.value = [
|
|
'Proof that OneNote can be updated programmatically from outside OneNote.',
|
|
`Created: ${new Date().toISOString()}`,
|
|
`Origin: ${location.origin}`,
|
|
].join('\n');
|
|
|
|
let msalClient = null;
|
|
let delegatedAccessToken = '';
|
|
let delegatedExpiresAt = 0;
|
|
|
|
async function getPocketBaseConfig() {
|
|
if (!pbConfigPromise) {
|
|
pbConfigPromise = fetch('/api/auth/pocketbase-config')
|
|
.then(async (resp) => {
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !data?.success || !data?.pbUrl) {
|
|
throw new Error(data?.message || 'PocketBase config unavailable');
|
|
}
|
|
return {
|
|
pbUrl: data.pbUrl,
|
|
collection: data.collection || PB_DEFAULT_AUTH_COLLECTION,
|
|
provider: data.provider || PB_DEFAULT_OAUTH_PROVIDER,
|
|
};
|
|
})
|
|
.catch((error) => {
|
|
pbConfigPromise = null;
|
|
throw error;
|
|
});
|
|
}
|
|
return pbConfigPromise;
|
|
}
|
|
|
|
async function ensurePocketBaseClient() {
|
|
if (pb) return pb;
|
|
const config = await getPocketBaseConfig();
|
|
pb = new PocketBase(config.pbUrl);
|
|
return pb;
|
|
}
|
|
|
|
function writeOutput(value) {
|
|
output.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
async function updatePbStatus() {
|
|
const client = await ensurePocketBaseClient();
|
|
if (client.authStore?.isValid) {
|
|
const email = client.authStore.model?.email || '(unknown user)';
|
|
pbStatus.textContent = `Logged in: ${email}`;
|
|
} else {
|
|
pbStatus.textContent = 'Not logged in';
|
|
}
|
|
}
|
|
|
|
function hasUsableToken() {
|
|
if (!delegatedAccessToken) return false;
|
|
if (!delegatedExpiresAt) return true;
|
|
return delegatedExpiresAt - 60000 > Date.now();
|
|
}
|
|
|
|
function updateOneNoteStatus() {
|
|
oneNoteAuthStatus.textContent = hasUsableToken()
|
|
? `Connected (expires ${new Date(delegatedExpiresAt).toLocaleTimeString()})`
|
|
: 'Disconnected';
|
|
}
|
|
|
|
async function loadHealth() {
|
|
try {
|
|
const resp = await fetch('/health');
|
|
const data = await resp.json();
|
|
healthStatus.textContent = resp.ok ? `OK (${data.pbDB || 'pb configured'})` : 'Unavailable';
|
|
} catch {
|
|
healthStatus.textContent = 'Unavailable';
|
|
}
|
|
}
|
|
|
|
async function loadGraphStatus() {
|
|
try {
|
|
const resp = await fetch('/api/graph/status');
|
|
const data = await resp.json().catch(() => ({}));
|
|
graphStatus.textContent = resp.ok && data?.active
|
|
? `Active (exp ${new Date(data.expiresOnISO).toLocaleTimeString()})`
|
|
: `Inactive (${data?.error || 'no token'})`;
|
|
} catch {
|
|
graphStatus.textContent = 'Inactive';
|
|
}
|
|
}
|
|
|
|
async function getMsalConfig() {
|
|
const resp = await fetch('/api/auth/msal-config');
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok || !data?.success) {
|
|
throw new Error(data?.message || 'MSAL config unavailable');
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function ensureMsalClient() {
|
|
if (msalClient) return msalClient;
|
|
if (!window.msal?.PublicClientApplication) {
|
|
throw new Error('MSAL browser library not loaded');
|
|
}
|
|
const config = await getMsalConfig();
|
|
msalClient = new window.msal.PublicClientApplication({
|
|
auth: {
|
|
clientId: config.clientId,
|
|
authority: config.authority,
|
|
redirectUri: config.redirectUri || window.location.origin,
|
|
},
|
|
cache: {
|
|
cacheLocation: 'localStorage',
|
|
storeAuthStateInCookie: false,
|
|
},
|
|
});
|
|
await msalClient.initialize();
|
|
return msalClient;
|
|
}
|
|
|
|
function captureToken(result) {
|
|
delegatedAccessToken = result?.accessToken || '';
|
|
delegatedExpiresAt = result?.expiresOn?.getTime?.() || 0;
|
|
updateOneNoteStatus();
|
|
}
|
|
|
|
async function handleMsalRedirect() {
|
|
const client = await ensureMsalClient();
|
|
const result = await client.handleRedirectPromise();
|
|
if (result?.accessToken) {
|
|
captureToken(result);
|
|
writeOutput({ message: 'OneNote connected successfully.' });
|
|
}
|
|
const accounts = client.getAllAccounts();
|
|
if (accounts.length && !hasUsableToken()) {
|
|
try {
|
|
const silent = await client.acquireTokenSilent({ scopes: SCOPES, account: accounts[0] });
|
|
captureToken(silent);
|
|
} catch {
|
|
updateOneNoteStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function connectOneNoteInteractive() {
|
|
const client = await ensureMsalClient();
|
|
const accounts = client.getAllAccounts();
|
|
await client.acquireTokenRedirect({
|
|
scopes: SCOPES,
|
|
account: accounts[0] || undefined,
|
|
prompt: 'select_account',
|
|
redirectStartPage: window.location.href,
|
|
});
|
|
}
|
|
|
|
async function refreshOneNoteToken() {
|
|
const client = await ensureMsalClient();
|
|
const accounts = client.getAllAccounts();
|
|
if (!accounts.length) {
|
|
throw new Error('Connect OneNote first');
|
|
}
|
|
const result = await client.acquireTokenSilent({ scopes: SCOPES, account: accounts[0] });
|
|
captureToken(result);
|
|
writeOutput({ message: 'OneNote token refreshed.' });
|
|
}
|
|
|
|
async function graphJson(url, options = {}) {
|
|
if (!hasUsableToken()) {
|
|
throw new Error('Connect OneNote first');
|
|
}
|
|
const resp = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
Authorization: `Bearer ${delegatedAccessToken}`,
|
|
Accept: 'application/json',
|
|
...(options.headers || {}),
|
|
},
|
|
});
|
|
const text = await resp.text();
|
|
let data = {};
|
|
try {
|
|
data = text ? JSON.parse(text) : {};
|
|
} catch {
|
|
data = { raw: text };
|
|
}
|
|
if (!resp.ok) {
|
|
throw new Error(JSON.stringify({ status: resp.status, data }, null, 2));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function loginPocketBase() {
|
|
writeOutput('Opening Microsoft login...');
|
|
const client = await ensurePocketBaseClient();
|
|
const config = await getPocketBaseConfig();
|
|
|
|
// Use the SDK's authWithOAuth2 realtime channel.
|
|
// Auth result is delivered back to this window via SSE from PocketBase,
|
|
// so it works regardless of how/where the login popup opens (including
|
|
// Edge app mode where external URLs open outside the app window).
|
|
const authData = await client.collection(config.collection).authWithOAuth2({
|
|
provider: config.provider,
|
|
urlCallback(url) {
|
|
const w = Math.min(900, window.screen.availWidth || 900);
|
|
const h = Math.min(680, window.screen.availHeight || 680);
|
|
const left = Math.floor(((window.screen.availWidth || 1280) - w) / 2);
|
|
const top = Math.floor(((window.screen.availHeight || 800) - h) / 2);
|
|
window.open(url, 'pb_oauth', `width=${w},height=${h},top=${top},left=${left},resizable=yes,menubar=no`);
|
|
},
|
|
});
|
|
|
|
await updatePbStatus();
|
|
writeOutput({
|
|
message: 'PocketBase login completed.',
|
|
user: authData?.record?.email || client.authStore.model?.email || null,
|
|
});
|
|
}
|
|
|
|
async function logoutPocketBase() {
|
|
const client = await ensurePocketBaseClient();
|
|
client.authStore.clear();
|
|
await updatePbStatus();
|
|
writeOutput({ message: 'PocketBase session cleared.' });
|
|
}
|
|
|
|
function buildNotePayload() {
|
|
const bodyPlain = noteBody.value.trim();
|
|
if (!bodyPlain) {
|
|
throw new Error('Note body is required');
|
|
}
|
|
const title = noteTitle.value.trim() || 'Untitled';
|
|
return {
|
|
noteId: globalThis.crypto?.randomUUID?.() || `note-${Date.now()}`,
|
|
title,
|
|
bodyPlain,
|
|
noteType: noteType.value || 'personal',
|
|
userEmail: pb?.authStore.model?.email || '',
|
|
jobNumber: noteJobNumber.value.trim(),
|
|
};
|
|
}
|
|
|
|
async function submitNoteToPrismNotes() {
|
|
const client = await ensurePocketBaseClient();
|
|
if (!client.authStore.isValid || !client.authStore.token) {
|
|
throw new Error('PocketBase login required');
|
|
}
|
|
const payload = buildNotePayload();
|
|
const resp = await fetch('/api/submit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
...payload,
|
|
pbToken: client.authStore.token,
|
|
}),
|
|
});
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) {
|
|
throw new Error(data?.message || 'Submit failed');
|
|
}
|
|
writeOutput({ message: 'Submitted to Prism Notes', response: data, payload });
|
|
}
|
|
|
|
async function syncCurrentNoteToOneNote() {
|
|
const client = await ensurePocketBaseClient();
|
|
if (!client.authStore.isValid || !client.authStore.token) {
|
|
throw new Error('PocketBase login required');
|
|
}
|
|
if (!hasUsableToken()) {
|
|
throw new Error('OneNote connection required');
|
|
}
|
|
|
|
const payload = buildNotePayload();
|
|
const resp = await fetch('/api/onenote/append', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${delegatedAccessToken}`,
|
|
'x-pb-token': client.authStore.token,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
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 });
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async function createProofPage() {
|
|
if (!hasUsableToken()) throw new Error('OneNote connection required');
|
|
const sectionId = sectionSelect.value.trim();
|
|
if (!sectionId) throw new Error('Click "Resolve site sections" first and select a section');
|
|
if (!resolvedSiteId) throw new Error('Site ID not resolved — click "Resolve site sections" first');
|
|
const title = proofTitle.value.trim() || `Prism Notes Proof ${new Date().toISOString()}`;
|
|
const message = proofBody.value.trim() || 'Proof page';
|
|
const bodyHtml = escapeHtml(message).replace(/\r\n|\r|\n/g, '<br/>');
|
|
const html = [
|
|
'<!DOCTYPE html>',
|
|
'<html>',
|
|
' <head>',
|
|
` <title>${escapeHtml(title)}</title>`,
|
|
' </head>',
|
|
' <body>',
|
|
` <p><strong>${escapeHtml(title)}</strong></p>`,
|
|
` <p>${bodyHtml}</p>`,
|
|
' </body>',
|
|
'</html>',
|
|
].join('');
|
|
|
|
const created = await graphJson(
|
|
`https://graph.microsoft.com/v1.0/sites/${resolvedSiteId}/onenote/sections/${sectionId}/pages`,
|
|
{ method: 'POST', headers: { 'Content-Type': 'text/html' }, body: html },
|
|
);
|
|
writeOutput({ message: 'Proof page created', siteId: resolvedSiteId, sectionId, created });
|
|
}
|
|
|
|
function bind(id, handler) {
|
|
document.getElementById(id).addEventListener('click', async () => {
|
|
try {
|
|
await handler();
|
|
} catch (error) {
|
|
writeOutput({ error: error?.message || String(error) });
|
|
}
|
|
});
|
|
}
|
|
|
|
bind('loginBtn', loginPocketBase);
|
|
bind('logoutBtn', logoutPocketBase);
|
|
bind('connectOneNoteBtn', connectOneNoteInteractive);
|
|
bind('refreshOneNoteTokenBtn', refreshOneNoteToken);
|
|
bind('submitNoteBtn', submitNoteToPrismNotes);
|
|
bind('syncOneNoteBtn', syncCurrentNoteToOneNote);
|
|
bind('resolveSectionsBtn', resolveSiteSections);
|
|
bind('createProofPageBtn', createProofPage);
|
|
|
|
ensurePocketBaseClient()
|
|
.then(() => updatePbStatus())
|
|
.catch((error) => writeOutput({ error: error?.message || String(error) }));
|
|
updateOneNoteStatus();
|
|
loadHealth();
|
|
loadGraphStatus();
|
|
handleMsalRedirect().catch((error) => writeOutput({ error: error?.message || String(error) }));
|
|
</script>
|
|
</body>
|
|
</html>
|