29 KiB
P44 OCR Viewer — Rebuild Prompts
Reference: ocr-viewer-audit.md — read this before starting any step. Each step must be confirmed working before the next step starts. Three files: p44-ocr-viewer.html / p44-ocr-viewer.css / p44-ocr-viewer.js
Before you start
Read ocr-viewer-audit.md in full. It contains the complete audit of the previous codebase, the root causes of every bug we hit, and the rebuild specification. Every architectural decision in these prompts comes from that document.
Key rules that must never be violated in this rebuild:
#text-outputis display only — never setcontentEditableon it#text-editoris the only edit surface — always a<textarea>, always plain text- No markdown parser — OCR output is plain text, display with
<pre> - No confidence scoring — removed by design
- Each step confirmed working before the next begins
Step 1 — HTML/CSS shell
No JavaScript. Layout and visual design only.
Create three empty files: p44-ocr-viewer.html, p44-ocr-viewer.css, p44-ocr-viewer.js
Build the complete HTML structure and CSS. Nothing should be functional.
HTML structure required:
#app (grid: toolbar / pass2-banner / panels / footer)
#toolbar
h1 "⚜ P44 OCR VIEWER"
#btn-load-ch button "▶ Calgary Highlanders — Sep 44"
PDF override: label.file-btn > input#input-pdf[type=file accept=.pdf]
OCR override: label.file-btn > input#input-text[type=file accept=.txt,.md]
JSON override: label.file-btn > input#input-json[type=file accept=.json]
#btn-example "Load sample"
#btn-sync-scroll "Sync Scroll" (class active by default)
role switcher: label "ROLE" + select#role-select
option value="pass1" "Pass 1 — Volunteer"
option value="pass2" "Pass 2 — Senior Editor"
#btn-clear-all "⚠ Clear all data" (red-tinted border and text)
#pass2-banner (display:none by default)
span#pass2-banner-text
button#btn-pass2-reset "↺ Reset Pass 2"
#panels (two-column flex)
Left panel div.panel
div.panel-header
span "Source Document — Page Scan"
span#zoom-label "100%"
button#btn-zoom-reset "1:1"
div.page-nav
button#btn-prev "◀" disabled
input#page-input type=number value=1 min=1
button#btn-next "▶" disabled
span#page-label "/ —"
div.panel-body#image-panel-body
div#image-placeholder
div#img-spinner style=display:none
div#img-ph-text "War diary scan loads automatically."
div#pdf-container style=display:none
canvas#pdf-canvas
div#overlay
Right panel div.panel
div.panel-header
span "OCR Text Output"
span#ocr-sync-label
div#correction-dot style=display:none title="This page has been edited"
button#btn-flag-verified class="tb-btn flag-btn" "✓ Verified"
button#btn-flag-review class="tb-btn flag-btn" "⚑ Review"
button#btn-flag-error class="tb-btn flag-btn" "✗ Error"
button#btn-flag-clear class="tb-btn flag-btn" "Clear"
button#btn-edit class="tb-btn" "Edit"
div.panel-body#text-panel-body
div#text-placeholder
div#txt-spinner style=display:none
div#txt-ph-text "OCR transcript loads automatically."
div#text-output style=display:none
textarea#text-editor style=display:none
#footer
span#status-msg "Ready."
span#flags-summary
span#kbd-hint "← → navigate v/r/e flag esc close"
CSS variables — use exactly these:
:root {
--bg: #1a1a2e;
--surface: #16213e;
--surface2: #0f3460;
--gold: #c8a84b;
--gold-dim: #8a6f2e;
--text: #ddd4bb;
--text-dim: #7a7260;
--text-muted:#4a4538;
--border: #2e2e4e;
--green: #4caf70;
--green-bg: #1a3326;
--amber: #e8a838;
--amber-bg: #2e2010;
--red: #d05050;
--red-bg: #2e1a1a;
--toolbar-h: 52px;
--footer-h: 34px;
}
CSS layout rules:
#app: CSS gridgrid-template-rows: var(--toolbar-h) auto 1fr var(--footer-h),height: 100vh,overflow: hidden#panels: flex row, each.paneltakeswidth: 50%,display: flex,flex-direction: column,overflow: hidden.panel-body:flex: 1,overflow-y: auto,overflow-x: hidden#pass2-banner:display: none, slim flex row,background: #1a1200,border-bottom: 1px solid var(--gold-dim),padding: 6px 14px,font-size: 11px,color: var(--amber)#text-editor:display: none,width: 100%,height: 100%,resize: none,font-family: Georgia serif,font-size: 12.5px,line-height: 1.75,background: var(--surface2),color: var(--text),border: none,border-top: 1px solid var(--gold-dim),padding: 16px 20px,box-sizing: border-box,outline: none#text-editor:focus:border-top-color: var(--gold).tb-btn:padding: 3px 9px,background: transparent,border: 1px solid var(--gold-dim),border-radius: 3px,color: var(--text-dim),font-family: Georgia serif,font-size: 10px,cursor: pointer,letter-spacing: 0.04em.tb-btn:hover:border-color: var(--gold),color: var(--gold).tb-btn.active:border-color: var(--gold),color: var(--gold).file-btn: same as.tb-btnbut as a<label>with hidden<input type="file">inside#btn-clear-all:border-color: #d05050,color: #d05050#btn-clear-all:hover:border-color: #e07070,color: #e07070.flag-btn.active:border-color: var(--gold),color: var(--gold)#correction-dot:width: 8px,height: 8px,border-radius: 50%,background: var(--gold),display: none,flex-shrink: 0.panel-header:display: flex,align-items: center,gap: 6px,padding: 0 10px,height: 38px,background: var(--surface),border-bottom: 1px solid var(--border),flex-shrink: 0,font-size: 9px,letter-spacing: 0.1em,color: var(--text-dim),text-transform: uppercase#toolbar:display: flex,align-items: center,gap: 8px,padding: 0 12px,background: var(--surface),border-bottom: 2px solid var(--gold-dim)#toolbar h1:font-size: 13px,font-weight: normal,letter-spacing: 0.1em,color: var(--gold),white-space: nowrap.divider:width: 1px,height: 26px,background: var(--border),flex-shrink: 0#footer:display: flex,align-items: center,justify-content: space-between,padding: 0 14px,background: var(--surface),border-top: 1px solid var(--border),font-size: 10px,color: var(--text-dim)#pdf-container:position: relative,display: inline-block#overlay:position: absolute,top: 0,left: 0,pointer-events: none.word-box:position: absolute,cursor: crosshair,border: 1px solid transparent,border-radius: 2px.word-box:hover, .word-box.highlight:background: rgba(200,168,75,0.18),border-color: var(--gold-dim).panel-placeholder:display: flex,flex-direction: column,align-items: center,justify-content: center,height: 100%,color: var(--text-dim),font-size: 12px,text-align: center,gap: 10px.spinner:width: 24px,height: 24px,border: 2px solid var(--border),border-top-color: var(--gold),border-radius: 50%,animation: spin 0.8s linear infinite@keyframes spin:to { transform: rotate(360deg) }#page-input:width: 48px,text-align: center,background: var(--surface2),border: 1px solid var(--border),color: var(--text),font-family: Georgia serif,font-size: 12px,padding: 2px 4px,border-radius: 3pxselect#role-select: same styling as.tb-btnbut as a select element,background: var(--surface2)#ocr-sync-label:font-size: 9px,color: var(--gold-dim),letter-spacing: 0.06em
Do not write any JavaScript yet.
Confirm when complete by listing every id and every CSS class defined.
Step 2 — PDF loading and page navigation
Add to p44-ocr-viewer.js only. Do not change HTML or CSS.
Add the following in this order:
- PDF.js worker setup:
if (typeof pdfjsLib !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
- Data paths:
const REAL_PDF_PATH = './Inputs/Calgary-Highlanders_War-Diary_Sep44.pdf';
const REAL_TEXT_PATH = './Inputs/ocr-output/Calgary-Highlanders_War-Diary_Sep44_olmocr.md';
- State object — only these properties to start:
const state = {
mode: 'none', // 'pdf' | 'image' | 'none'
pdfDoc: null,
currentPage: 1,
totalPages: 0,
zoomScale: 1.0, // Ctrl+scroll zoom multiplier (0.5–4.0)
wordData: [],
rendering: false,
lastRenderTime: 0,
ocrPages: {}, // { [pageNum]: string } — populated in Step 3
flags: {}, // populated in Step 5
corrections: {}, // populated in Step 6
editMode: false, // populated in Step 6
};
-
All DOM refs (getElementById for every id in the HTML)
-
escH(s)utility function -
setStatus(msg, warn)utility function -
showImgSpinner(on),showTxtSpinner(on),showImgPlaceholder(html),showTxtPlaceholder(html)utility functions -
loadPDFBuffer(arrayBuffer)— loads PDF into PDF.js, setsstate.pdfDoc,state.totalPages,state.mode = 'pdf', callsrenderCurrentPage() -
renderCurrentPage()— fits page to panel width, appliesstate.zoomScale, uses DPR for HiDPI, renders to#pdf-canvas, callsbuildPDFWordOverlay(). WrapshowOCRPage()call in try/catch (showOCRPage is added in Step 3). Include_pendingPagequeue and debounce guard. -
buildPDFWordOverlay(page, vp, dpr)— builds word boxes fromstate.wordDataif loaded, otherwise leaves overlay blank -
makeWordBox(id, x, y, w, h, title)— creates positioned div with hover highlight -
goToPage(n)— validates range, guards against re-render of same page, callsrenderCurrentPage(). No edit mode save yet (added in Step 6). -
_pendingPage = nullmodule variable -
updateZoomLabel()— updates#zoom-labeltext -
Event listeners:
#btn-prevclick →goToPage(state.currentPage - 1)#btn-nextclick →goToPage(state.currentPage + 1)#page-inputchange →goToPage(parseInt(value))#btn-zoom-resetclick → resetstate.zoomScaleto 1.0, re-renderwindow resize→ re-render if PDF loaded and not rendering#input-pdfchange → read file, callloadPDFBuffer()#input-jsonchange → read file, parse JSON, setstate.wordData, re-render
-
Ctrl+scroll zoom on
#image-panel-body:- MUST check
e.ctrlKey— only zoom when Ctrl is held - Only call
e.preventDefault()whene.ctrlKeyis true - Without Ctrl: let the event pass through normally for scrolling
- With Ctrl: adjust
state.zoomScaleby ±0.1, clamp to 0.5–4.0, re-render - After re-render, scroll to keep cursor anchor point in view
- MUST check
-
DOMContentLoadedauto-load:
window.addEventListener('DOMContentLoaded', () => {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
setTimeout(loadCalgaryHighlanders, 200);
}
});
loadCalgaryHighlanders()— fetches PDF only for now (OCR text fetch added in Step 3). UsesPromise.allSettled.
Test: PDF loads, pages navigate with buttons and keyboard input, zoom works with Ctrl+scroll, normal scroll works without Ctrl.
Step 3 — OCR text loading and plain text display
Add to p44-ocr-viewer.js only. Do not change HTML or CSS.
-
parseOCRByPage(rawText):- Splits on
## Page Nheadings - Stores content between headings in
state.ocrPages[N] - Does NOT include the
## Page Nheading line in the content buf = []notbuf = [line]
- Splits on
-
loadOCRText(rawText):- Calls
parseOCRByPage(rawText) - Does NOT call
showOCRPage()— only calls it ifstate.pdfDocis already loaded - This prevents double-render on startup
- Calls
-
showOCRPage(pageNum):- Clears
textOutput.innerHTML = '' - Sets
textOutput.dataset.page = pageNum - Hides placeholder, shows
textOutput - If no OCR pages loaded: shows "OCR text not loaded yet" message
- If no content for this page: shows "No OCR text for page N" message
- Gets display text:
corrections[pageNum]?.pass1 || state.ocrPages[pageNum](corrections object added in Step 6, guard with optional chaining for now) - Renders as:
<pre style="white-space:pre-wrap;font-family:inherit;font-size:12.5px;line-height:1.75;padding:16px 20px;">{escH(displayText)}</pre> - Scrolls text panel to top
- Updates
#ocr-sync-labeltext
- Clears
-
Update
loadCalgaryHighlanders()to also fetchREAL_TEXT_PATHand callloadOCRText() -
Add
#input-textchange event listener →loadOCRText() -
Add
showOCRPage(state.currentPage)call insiderenderCurrentPage()after PDF page renders
Test: OCR text loads alongside PDF, correct page shown for each page number, no ## Page N heading visible in text panel, plain text renders correctly.
Step 4 — Synchronized scrolling
Add to p44-ocr-viewer.js only. Do not change HTML or CSS.
- Module variables:
let _syncScrolling = false;
let _syncActive = true;
-
onImgScroll()— when left panel scrolls, scroll right panel to same proportional position. UserequestAnimationFrameto reset_syncScrollingflag. -
onTxtScroll()— when right panel scrolls, scroll left panel to same proportional position. -
Attach scroll listeners to
#image-panel-bodyand#text-panel-body -
#btn-sync-scrollclick → toggle_syncActive, toggle.activeclass on button
Test: Scrolling either panel scrolls the other proportionally. Toggle button disables sync. Normal mouse wheel scroll works on left panel (no zoom without Ctrl).
Step 5 — Flag system
Add to p44-ocr-viewer.js only. Do not change HTML or CSS.
-
updateFlagButtons(pageNum)— toggles.activeclass on flag buttons based onstate.flags[pageNum] -
updateFlagsSummary()— counts verified/review/error flags, updates#flags-summarytext -
setFlag(type)— sets or clearsstate.flags[state.currentPage], callsupdateFlagButtons(),updateFlagsSummary(),saveFlags() -
saveFlags():- POST to
/save-flagswithstate.flagsas JSON body - On failure: fallback to
localStorage.setItem('p44-flags', ...)
- POST to
-
loadFlagsOnStartup():- Fetch
/flags.json - On failure: fallback to
localStorage.getItem('p44-flags') - Call
updateFlagsSummary()after loading
- Fetch
-
clearPass1Queue()— removes allverifiedflags, keeps review/error, confirm dialog required -
Event listeners:
#btn-flag-verified→setFlag('verified')#btn-flag-review→setFlag('review')#btn-flag-error→setFlag('error')#btn-flag-clear→setFlag(null)#btn-clear-p1-queue→clearPass1Queue()
-
Call
loadFlagsOnStartup()onDOMContentLoaded -
Call
updateFlagButtons(pageNum)at the end ofshowOCRPage()
Test: Flags save and persist across refresh. Footer summary updates. Clear queue removes only verified flags.
Step 6 — Edit mode (Pass 1 only)
This is the most critical step. Read the architecture rules before starting.
Architecture rules — must not be violated:
#text-outputis NEVER set tocontentEditable. It is display only.#text-editor(<textarea>) is the ONLY edit surface.- When edit mode is active: hide
#text-output, show#text-editor - When edit mode is inactive: hide
#text-editor, show#text-output #text-editoralways contains plain text — never HTML
Correction data model:
state.corrections[pageNum] = {
original: string, // raw OCR snapshot, taken ONCE on first edit-open, never overwritten
pass1: string, // Pass 1 saved text (absent if unchanged from original)
pass2: string, // Pass 2 saved text (added in Step 8, absent for now)
}
Add to p44-ocr-viewer.js:
-
updateCorrectionIndicator(pageNum)— shows/hides#correction-dotbased on whethercorrections[pageNum]?.pass1exists -
postCorrection(pageNum, corrObj):- POST to
/save-correctionswith{ page: pageNum, correction: corrObj } - On failure: fallback to localStorage
p44-corrections
- POST to
-
loadCorrectionsOnStartup():- Fetch
/corrections.json - On failure: fallback to localStorage
- Remove any entries where both
pass1andpass2are absent - Set
state.corrections
- Fetch
-
enterEditMode():- Get pageNum = state.currentPage - If corrections[pageNum] does not exist: create { original: state.ocrPages[pageNum].trim() } - If corrections[pageNum].original does not exist: set it to state.ocrPages[pageNum].trim() - Set textEditor.value = corrections[pageNum].pass1 || corrections[pageNum].original - Set textEditor.dataset.page = pageNum - Hide textOutput (display:none) - Show textEditor (display:block) - Focus textEditor - Set state.editMode = true - Update btnEdit text to "Editing", add .active class -
exitEditMode():- If !state.editMode: return - Get pageNum = parseInt(textEditor.dataset.page) - Get editedText = textEditor.value.trim() - Get originalText = corrections[pageNum]?.original || state.ocrPages[pageNum]?.trim() || '' - If editedText !== originalText AND editedText is not empty: corrections[pageNum].pass1 = editedText Else: delete corrections[pageNum].pass1 - If neither pass1 nor pass2 exist: delete corrections[pageNum] entirely - Call postCorrection(pageNum, corrections[pageNum] || null) - Call updateCorrectionIndicator(pageNum) - Hide textEditor (display:none), clear textEditor.value - Show textOutput (display:block) - Set state.editMode = false - Update btnEdit text to "Edit", remove .active class - Call showOCRPage(pageNum) to re-render display with saved correction -
Update
goToPage()— callexitEditMode()at the very top before any other logic -
#btn-editclick → toggle edit mode -
Update
showOCRPage()display text logic:const corr = state.corrections[pageNum]; const displayText = (corr?.pass1) || state.ocrPages[pageNum] || ''; -
Call
loadCorrectionsOnStartup()onDOMContentLoaded -
Call
updateCorrectionIndicator(pageNum)at end ofshowOCRPage()
Test thoroughly before proceeding:
- Enter edit mode: textarea appears, display hidden
- Make a change and exit: display re-renders with correction, gold dot appears
- Navigate away and back: correction persists, dot still shows
- Enter and exit without changing: no correction saved, no dot
- Refresh page: correction loads from localStorage/server, dot shows correctly
- Confirm
## Page Nnever appears in the textarea
Step 7 — Corrections persistence in launch_viewer.py
Add to launch_viewer.py only. Do not change any viewer files.
Add a do_POST method to QuietHandler that handles these endpoints:
-
POST /save-flags— read JSON body, write toflags.jsonin ROOT -
POST /save-corrections— read JSON body ({ page, correction }), load existingcorrections.json, update the entry forpage, write back -
POST /save-pass2-flags— read JSON body, write topass2_flags.jsonin ROOT -
POST /clear-all-data— deleteflags.json,corrections.json,pass2_flags.jsonif they exist, respond 200
All endpoints:
- Read body with
self.rfile.read(int(self.headers['Content-Length'])) - Parse with
json.loads() - Respond
200 OKon success - Respond
400on parse error - Never crash the server on bad input — wrap in try/except
Test: Save a flag, restart the server, reload viewer — flag persists from file not just localStorage.
Step 8 — Role switcher and Pass 2 mode
Add to p44-ocr-viewer.js only. Do not change HTML or CSS.
- Module variables:
let currentRole = 'pass1';
let pass2Flags = {};
const PASS1_LABELS = { verified: '✓ Verified', review: '⚑ Review', error: '✗ Error' };
const PASS2_LABELS = { verified: '✓ Pass 2 approved', review: '⚑ Needs work', error: '✕ Rejected' };
-
getVerifiedPages()— returns sorted array of page numbers wherestate.flags[n] === 'verified' -
updatePass2NavButtons()— in Pass 2 mode, disables prev/next if no verified pages before/after current -
updatePass2Banner()— updates banner text with count of verified pages ready for review -
applyRole(role):- Sets
currentRole - Saves to
localStorage('p44-role') - Shows/hides
#pass2-banner - Updates flag button labels to PASS1 or PASS2 labels
- Calls
updatePass2Banner()if pass2 - Re-renders current page via
showOCRPage(state.currentPage)
- Sets
-
Update
#btn-prev/#btn-nextclick handlers — in Pass 2 mode, jump between verified pages only -
Update
showOCRPage()— at the end, ifcurrentRole === 'pass2', callenterEditMode()if not already in edit mode. Also show pass2-specific display text:corr?.pass2 || corr?.pass1 || state.ocrPages[pageNum] -
savePass2Flags()— POST to/save-pass2-flags, fallback to localStoragep44-pass2-flags -
loadPass2FlagsOnStartup()— fetch/pass2_flags.json, fallback to localStorage -
Update
setFlag()— in Pass 2 mode, write topass2Flagsand callsavePass2Flags()instead ofstate.flags -
Update
updateFlagButtons()— read frompass2Flagsin Pass 2 mode -
resetPass2()— clearspass2Flagsand all corrections, confirm dialog, cannot be undone -
#btn-pass2-resetclick →resetPass2() -
#role-selectchange →applyRole(value) -
On
DOMContentLoaded, restore role:applyRole(localStorage.getItem('p44-role') || 'pass1') -
Call
loadPass2FlagsOnStartup()onDOMContentLoaded
Test: Switch roles, Pass 2 nav jumps between verified pages, Pass 2 flags stored separately, role persists on refresh.
Step 9 — Pass 2 change summary panel
Add to HTML, CSS, and JS.
HTML — add inside #text-panel-body after #text-placeholder, before #text-output:
<div id="pass2-change-summary" style="display:none;"></div>
CSS — add:
#pass2-change-summary {
display: none;
padding: 10px 20px;
background: var(--surface2);
border-bottom: 1px solid var(--gold-dim);
flex-shrink: 0;
font-size: 11px;
}
#pass2-change-summary .cs-title {
font-size: 10px;
color: var(--gold-dim);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 6px;
}
#pass2-change-summary .cs-list {
color: var(--text-dim);
line-height: 1.8;
max-height: 100px;
overflow-y: auto;
margin-bottom: 8px;
font-family: Georgia, serif;
}
#pass2-change-summary .cs-changed { color: var(--amber); }
#pass2-change-summary .cs-added { color: var(--green); }
#pass2-change-summary .cs-deleted { color: var(--red); }
#pass2-change-summary .cs-actions { display: flex; gap: 8px; }
.cs-revert-btn {
font-size: 10px;
font-family: Georgia, serif;
padding: 3px 10px;
border-radius: 3px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-dim);
cursor: pointer;
letter-spacing: 0.04em;
}
.cs-revert-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
.cs-revert-btn.danger { border-color: var(--red); color: var(--red); }
JS — add:
-
buildChangeSummary(originalText, pass1Text):- Split both strings on whitespace:
str.trim().split(/\s+/) - Walk both arrays simultaneously
- Produce array of
{ type: 'changed'|'added'|'deleted', from?, to?, word? } - Cap at 50 changes, add
{ type: 'truncated' }if over - Return empty array if strings are identical
- Split both strings on whitespace:
-
showPass2ChangeSummary(pageNum):- Get
corr = state.corrections[pageNum] - If no
corr?.pass1orcurrentRole !== 'pass2': hide panel, return - Build changes from
corr.originalvscorr.pass1 - If no changes: hide panel, return
- Render
cs-titlewith change count - Render
cs-listwith each change coloured by type - Render two action buttons: "↺ Revert to Pass 1" and "⚠ Revert to original OCR"
- Show panel
- Wire revert buttons:
- Revert to Pass 1:
confirm()dialog, settextEditor.value = corr.pass1, status message - Revert to original:
prompt('Type REVERT to confirm'), if confirmed deletecorrections[pageNum], callpostCorrection(pageNum, null), update indicator, hide summary, status message
- Revert to Pass 1:
- Get
-
Call
showPass2ChangeSummary(state.currentPage)at end ofenterEditMode() -
Hide
#pass2-change-summaryat start ofexitEditMode()
Test: Switch to Pass 2, navigate to a page with Pass 1 corrections — change summary appears above textarea showing exactly what was changed. Revert to Pass 1 restores Pass 1 text. Revert to original requires typing REVERT and clears all corrections.
Step 10 — Keyboard navigation
Add to p44-ocr-viewer.js only.
-
document.addEventListener('keydown', e => {...}) -
Guard: if
state.editModeis true, return immediately (let all keys go to textarea) -
Guard: if focus is on any
INPUT,TEXTAREA, orSELECT, return -
Key mappings:
ArrowRightorl→goToPage(state.currentPage + 1)ArrowLeftorj→goToPage(state.currentPage - 1)v→setFlag('verified')r→setFlag('review')e→setFlag('error')Escape→ if edit mode active:exitEditMode(). Otherwise: nothing. Do NOT referencezoomLens— that element does not exist.
Test: Arrow keys navigate pages, v/r/e set flags, Escape exits edit mode, no errors thrown.
Step 11 — Clear all data
Add to p44-ocr-viewer.js and launch_viewer.py.
JS — add clearAllLocalData():
function clearAllLocalData() {
if (!confirm(
'Clear ALL saved data?\n\n' +
'This will wipe:\n' +
' • All Pass 1 and Pass 2 flags\n' +
' • All text corrections\n' +
' • Role setting\n\n' +
'This cannot be undone.'
)) return;
localStorage.removeItem('p44-corrections');
localStorage.removeItem('p44-flags');
localStorage.removeItem('p44-pass2-flags');
localStorage.removeItem('p44-role');
state.corrections = {};
state.flags = {};
pass2Flags = {};
applyRole('pass1');
updateFlagButtons(state.currentPage);
updateFlagsSummary();
updateCorrectionIndicator(state.currentPage);
if (state.pdfDoc) showOCRPage(state.currentPage);
fetch('/clear-all-data', { method: 'POST' }).catch(() => {});
setStatus('All saved data cleared — fresh start.');
}
Wire #btn-clear-all click → clearAllLocalData()
launch_viewer.py — confirm /clear-all-data endpoint exists from Step 7. If not added yet, add it now.
Test: Click clear, confirm, all flags gone, all corrections gone, role reset, page re-renders as raw OCR, server files deleted.
Step 12 — Sample data
Add to p44-ocr-viewer.js only.
-
Add
SAMPLE_TEXTconstant (plain prose — no markdown) -
Add
SAMPLE_JSONconstant (word bbox data) -
buildSampleImageDataURL()— canvas-drawn sample page image -
loadSampleData():- Sets
state.mode = 'image' - Draws sample image to canvas
- Loads sample JSON word data
- Calls
loadOCRText(SAMPLE_TEXT) - Status:
'Sample data loaded.'— do not reference any removed features
- Sets
-
#btn-exampleclick →loadSampleData()
Test: Sample button loads image and OCR text, page displays correctly.
Step 13 — Final cleanup and launch_viewer.py review
Review all three files against this checklist:
JS cleanup:
- No reference to
zoomLensanywhere - No
contentEditableset on#text-outputanywhere - No
convertMarkdown()orinlineMd()function - No
sanitizeCorrection()function - No
stripHTML()function - No word-level diff functions (
wordDiff,lcsWords,tokenizeWords,renderDiff) - No diff legend DOM refs or event listeners
state.scaleremoved (it was dead — there is no scale dropdown)state.ocrRawFullremoved (dead — never read)p44_diff_legend_dismissedNOT inclearAllLocalData()(key was never written)- localStorage keys are consistent:
p44-flags,p44-pass2-flags,p44-corrections,p44-role
launch_viewer.py review:
/save-flagsPOST handler exists/save-correctionsPOST handler exists/save-pass2-flagsPOST handler exists/clear-all-dataPOST handler exists- All handlers wrapped in try/except
Cache-Control: no-storeheaders present on all responses
Final test — full workflow:
- Load Calgary Highlanders
- Navigate to page 10
- Flag as Verified (Pass 1)
- Enter edit mode, make a correction, exit
- Refresh — correction and flag persist
- Switch to Pass 2
- Navigate to page 10 (only verified page)
- Change summary shows Pass 1 edits
- Make a Pass 2 correction
- Revert to original (type REVERT)
- Clear all data
- Refresh — everything is gone
End of rebuild prompts.