Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5a0ff40
Hydrate from seed when fetch fails
cubap Feb 6, 2026
a71968c
runVaulttest
cubap Feb 6, 2026
de6307b
vault all fetches
cubap Feb 6, 2026
36b1e91
Prefetch manifests before resolving canvases
cubap Feb 6, 2026
2953bea
Use vault and prefetch manifests; enhance Vault
cubap Feb 6, 2026
ae6d1ec
Process IIIF resources and simplify ID normalization
cubap Feb 9, 2026
4ec5596
Update interfaces/transcription/index.js
cubap Feb 9, 2026
c1e91b1
Update components/read-only-transcribe/index.js
cubap Feb 9, 2026
973e4cf
Guard against empty URI in vault fetch to prevent unnecessary request…
Copilot Feb 9, 2026
78bd3b0
Add prefetchManifests and prefetchCollections aliases with explicit t…
Copilot Feb 9, 2026
ab4db40
Eliminate N+1 fetches for embedded IIIF resources in vault (#439)
Copilot Feb 9, 2026
db8698c
Add explicit IIIF v2 prefixed types to resource set (#440)
Copilot Feb 9, 2026
b81735c
Update vault.js
cubap Feb 9, 2026
5185901
Merge branch 'vaulting' of https://github.com/CenterForDigitalHumanit…
cubap Feb 9, 2026
6ec5ad1
deprecate these
thehabes Feb 9, 2026
71420f6
strip out TPEN.js and vault.js
cubap Feb 9, 2026
489d059
Import vault in read-only-transcribe component
cubap Feb 9, 2026
de19429
This will work here (#443)
thehabes Feb 9, 2026
32fc6ae
Early guard to avoid NPEs when this.#transcriptions cannot be used (#…
thehabes Feb 9, 2026
01f8684
Address Issues 13-17 from static review: Fix vault consistency, error…
Copilot Feb 9, 2026
2bafa53
Fix vault.js issues 8-11: Hoist constants, fix noCache, clone data, a…
cubap Feb 9, 2026
45d2cb7
Update Conditionals to avoid NPEs (#447)
thehabes Feb 9, 2026
f6cad0d
Use vault.getWithFallback for fetching (#449)
cubap Feb 9, 2026
3be1a6e
refactor manage columns to use vault (#450)
thehabes Feb 9, 2026
fb0e63d
hotfix for error messaging
thehabes Feb 9, 2026
a7439b9
No 'Auto Parse' for now
thehabes Feb 10, 2026
c13076e
No 'Auto Parse' for now
thehabes Feb 10, 2026
3b115b2
Fix so CONTRIBUTOR users see the link into the /annotator interface
thehabes Feb 10, 2026
122ff84
See transcription text in .transcription-input on the transcription i…
thehabes Feb 10, 2026
e51a257
Don't change @deprecated components
thehabes Feb 10, 2026
250e76d
Changes while reviewing
thehabes Feb 11, 2026
dca5a14
Changes during review
thehabes Feb 11, 2026
36c2e69
Changes during review
thehabes Feb 11, 2026
a79b190
zooming on view transcription interface
thehabes Feb 11, 2026
e0f47d7
Don't let view transcription interface be larger than the viewport an…
thehabes Feb 11, 2026
67919cf
double right-click to zoom out
thehabes Feb 11, 2026
3dd9bb1
double right-click to zoom out
thehabes Feb 11, 2026
be53732
small change for accessibility
thehabes Feb 11, 2026
a2236c5
small change for accessibility. Add some consistency around how thes…
thehabes Feb 11, 2026
34c24ae
small change for accessibility. Add some consistency around how thes…
thehabes Feb 11, 2026
bd667ad
whoops make it prettier
thehabes Feb 11, 2026
1eb3551
changes while reviewing
thehabes Feb 11, 2026
8e1a280
remove references to elements that are no longer used
thehabes Feb 11, 2026
11b30bb
Extra defensive for an extra chance to find the image
thehabes Feb 11, 2026
89a1108
some cleanup
thehabes Feb 11, 2026
e54b9b3
Changes while reviewing
thehabes Feb 11, 2026
7358d64
Changes while reviewing
thehabes Feb 11, 2026
3cb3bf4
Changes while reviewing
thehabes Feb 11, 2026
fffa5d5
Changes while reviewing
thehabes Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ TPEN3 Interfaces is a JavaScript library that provides web components and API cl
│ ├── index.css # Main stylesheet
│ ├── manage/ # Management page styles
│ └── collaborators/ # Collaborator styles
├── js/ # Utility scripts (redirect, vault)
├── js/ # Utility scripts (redirect, vault, utils)
├── assets/ # Static assets (icons, images, logos)
├── _classes/ # Jekyll class documentation
├── _cookbook/ # Jekyll cookbook examples
Expand Down Expand Up @@ -169,6 +169,43 @@ CONFIG.TPEN3URL // TPEN 3 main URL
// 4. Defaults to 'dev'
```

### Vault (`js/vault.js`)
Singleton caching layer for IIIF resources. **This is a custom utility, not the `@iiif/helpers` Vault.**
All components should use the vault instead of raw `fetch()` for IIIF resources (canvases, manifests, annotation pages, annotations, etc.).

```javascript
import vault from '../../js/vault.js'

// Core methods:
vault.get(item, itemType, noCache) // Fetch with dual-layer cache (memory + localStorage)
vault.set(item, itemType) // Store in both caches
vault.delete(item, itemType) // Remove from both caches
vault.clear(itemType) // Purge all of a given type
vault.all() // Get all cached resources

// Fallback methods:
vault.getWithFallback(item, itemType, manifestUrls, noCache) // get() + prefetch manifests on miss
vault.prefetchManifests(urls) // Batch prefetch manifests
vault.prefetchDocuments(items, docType) // Batch prefetch any document type
vault.prefetchCollections(items) // Batch prefetch collections
```

**Key behaviors:**
- **Dual-layer cache**: in-memory `Map` for speed + `localStorage` for persistence across reloads
- **BFS hydration**: when a resource is fetched, embedded IIIF sub-resources are recursively cached individually via `structuredClone()`. The parent stores minimal stubs (`{id, type, label}`) for non-array properties.
- **In-flight deduplication**: concurrent `get()` calls for the same resource share a single network request
- **Seed fallback**: when `item` is an object and the network fetch fails, the object is hydrated and cached as a fallback
- **IIIF v2 + v3 types**: recognizes both unprefixed (v3: `Canvas`, `Manifest`) and prefixed (v2: `sc:Canvas`, `sc:Manifest`) types
- **`noCache` flag**: bypasses in-memory and localStorage lookups to force a fresh fetch

### URL Resolution (`js/utils.js`)
`urlFromIdAndType(id, type, { projectId, pageId, layerId })` resolves resource IDs to fetchable URLs.

- Full URLs (http/https) are returned as-is
- TPEN resources (`annotationpage`, `annotation`, `annotationcollection`) are resolved to the services API
- **External IIIF types (`canvas`, `manifest`, `collection`) return `null`** — these must already be full URLs or will be found as embedded data via manifest prefetch
- Returns `null` (not empty string) when resolution is impossible

## API Endpoints

The API follows RESTful conventions. Base URL is determined by environment (dev: `https://dev.api.t-pen.org`, prod: `https://api.t-pen.org`).
Expand Down Expand Up @@ -458,14 +495,7 @@ node --test api/__tests__/project.test.js # Run specific test
## Configuration & Deployment

### Environment Configuration
Configuration is managed via `api/config.js`:
- **Development** (`dev`): Uses dev.api.t-pen.org, devstore.rerum.io
- **Production** (`prod`): Uses api.t-pen.org, store.rerum.io

Set environment via:
1. `globalThis.TPEN_ENV = 'prod'` before importing
2. `<meta name="tpen-env" content="prod">` in HTML
3. `process.env.TPEN_ENV` for server-side
See [Configuration (`api/config.js`)](#configuration-apiconfigjs) above for environment detection and URLs.

### Jekyll Configuration (`_config.yml`)
```yaml
Expand Down Expand Up @@ -524,7 +554,31 @@ Automated deployment pipeline:

## Common Patterns & Best Practices

### Error Handling
### IIIF Resource Fetching
Always use `vault` for IIIF resources. Never use raw `fetch()` for canvases, manifests, annotation pages, or annotations.

```javascript
import vault from '../../js/vault.js'

// Simple fetch — checks cache, then network
const canvas = await vault.get(canvasURI, 'canvas')

// Fetch with manifest fallback — if not found, prefetches project manifests
// and retries (useful when canvas/page might be embedded in manifest)
const page = await vault.getWithFallback(pageID, 'annotationpage', TPEN.activeProject?.manifest, true)
const canvas = await vault.getWithFallback(canvasID, 'canvas', TPEN.activeProject?.manifest)

// Force fresh fetch (bypass cache) with noCache=true
const freshPage = await vault.get(pageID, 'annotationpage', true)

// Resolve annotations that may be stubs (have target but no body)
let annotation = item
if (!annotation?.body) {
annotation = await vault.get(item, 'annotation', true)
}
```

### Error Handling (TPEN API)
```javascript
try {
const response = await fetch(url)
Expand Down Expand Up @@ -571,18 +625,19 @@ try {
1. **Authentication is Critical**: Always ensure authentication is properly handled before making API calls
2. **Web Components Lifecycle**: Respect the component lifecycle - initialization in connectedCallback, cleanup in disconnectedCallback
3. **Event Bubbling**: Use bubbles: true and composed: true for events that need to cross shadow DOM boundaries
4. **IIIF Standards**: When working with manifests, follow IIIF Presentation API standards
4. **IIIF Resources via Vault**: Always use `vault.get()` or `vault.getWithFallback()` for IIIF resources — never raw `fetch()`. The vault handles caching, fallback to manifest data, and IIIF v2/v3 compatibility
5. **Project Context**: Projects are the central organizing unit - users create projects, add manifests, and collaborate
6. **Error Messages**: Provide clear, user-friendly error messages, not raw API errors
7. **Testing**: Always write tests for new functionality, especially for API interactions
8. **CSS Encapsulation**: Remember that shadow DOM encapsulates styles - use CSS custom properties for theming

### Common Issues to Avoid:
- Don't forget to attach authentication to fetch requests
- Don't forget to attach authentication to fetch requests for TPEN API calls
- Don't manipulate DOM directly in components - use render() method
- Don't store sensitive data in localStorage beyond auth tokens
- Don't make synchronous API calls - always use async/await
- Don't forget to clean up event listeners in disconnectedCallback
- Don't import `@iiif/helpers` Vault — `components/default-transcribe` still uses it and is `@deprecated`

### Development Tips:
- Use the browser's Developer Tools to inspect web components
Expand Down
51 changes: 32 additions & 19 deletions components/annotorious-annotator/line-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { detectTextLinesCombined } from "./detect-lines.js"
import { v4 as uuidv4 } from "https://cdn.skypack.dev/uuid@9.0.1"
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
import { onProjectReady } from '../../utilities/projectReady.js'
import vault from '../../js/vault.js'
import '../page-selector/index.js'

class AnnotoriousAnnotator extends HTMLElement {
Expand Down Expand Up @@ -282,7 +283,7 @@ class AnnotoriousAnnotator extends HTMLElement {
tpen-page-selector {
position: absolute;
display: none;
top: 60px;
top: 10px;
right: 10px;
z-index: 9;
}
Expand Down Expand Up @@ -378,6 +379,8 @@ class AnnotoriousAnnotator extends HTMLElement {
this.renderCleanup.run()

this.renderCleanup.onElement(this.shadowRoot.querySelector("#autoParseBtn"), "click", async () => {
// TODO this needs testing before we are ready for users to use it
return
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Auto Parse click handler immediately returns before the try block, leaving a large unreachable block of code registered on every render. If the feature is intentionally disabled, it would be clearer to not register the listener (and/or hide/remove the button) behind a feature flag; otherwise remove the early return so the handler can run.

Suggested change
return

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah it's supposed to be dead like that for now until we want to use it.

try {
if (typeof cv === "undefined") {
await new Promise((resolve, reject) => {
Expand Down Expand Up @@ -553,7 +556,7 @@ class AnnotoriousAnnotator extends HTMLElement {

/**
* Fetch a Canvas URI and check that it is a Canvas object. Pass it forward to render the Image into the interface.
* Be prepared to recieve presentation api 2+
* Be prepared to receive presentation api 2+
*
* FIXME
* Give users a path when Canvas URIs do not resolve or resolve to something unexpected.
Expand All @@ -562,20 +565,15 @@ class AnnotoriousAnnotator extends HTMLElement {
*/
async processCanvas(uri) {
if (!uri) return
// TODO Vault me?
const resolvedCanvas = await fetch(uri)
.then(r => {
if (!r.ok) throw r
return r.json()
})
.catch(e => {
this.shadowRoot.innerHTML = `
<h3>Canvas Error</h3>
<p>The Canvas within this Page could not be loaded.</p>
<p> ${e.status ?? e.code}: ${e.statusText ?? e.message} </p>
`
throw e
})
let resolvedCanvas = await vault.getWithFallback(uri, 'canvas', TPEN.activeProject?.manifest)
if (!resolvedCanvas) {
this.shadowRoot.innerHTML = `
<h3>Canvas Error</h3>
<p>The Canvas within this Page could not be loaded.</p>
<p>The Canvas could not be resolved or is invalid.</p>
`
return
}
const context = resolvedCanvas["@context"]
if (!context?.includes("iiif.io/api/presentation/3/context.json")) {
console.warn("The Canvas object did not have the IIIF Presentation API 3 context and may not be parseable.")
Expand Down Expand Up @@ -612,14 +610,14 @@ class AnnotoriousAnnotator extends HTMLElement {
const canvasID = resolvedCanvas["@id"] ?? resolvedCanvas.id
this.#canvasID = canvasID
let fullImage = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.id
if (!fullImage) fullImage = resolvedCanvas?.images?.[0]?.resource?.["@id"]
if (!fullImage) fullImage = resolvedCanvas?.images?.[0]?.resource?.["@id"] ?? resolvedCanvas?.images?.[0]?.resource?.id
let imageService = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.service?.id
if (!imageService) imageService = resolvedCanvas?.images?.[0]?.resource?.service?.["@id"]
if (!fullImage) {
throw new Error("Cannot Resolve Canvas Image", { "cause": "The Image is 404 or unresolvable." })
}
let imgx = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.width
if (!imgx) imgx = resolvedCanvas?.images[0]?.resource?.width
if (!imgx) imgx = resolvedCanvas?.images?.[0]?.resource?.width
let imgy = resolvedCanvas?.items?.[0]?.items?.[0]?.body?.height
if (!imgy) imgy = resolvedCanvas?.images?.[0]?.resource?.height
this.#imageDims = [imgx, imgy]
Expand Down Expand Up @@ -689,6 +687,20 @@ class AnnotoriousAnnotator extends HTMLElement {
dblClickToZoom: true
}
})

// Double right-click to zoom out
let lastRightClickTime = 0
this.renderCleanup.onElement(this.#annotoriousContainer, 'contextmenu', (e) => {
e.preventDefault()
const now = Date.now()
if (now - lastRightClickTime < 400) {
this.#osd.viewport.zoomBy(0.5)
lastRightClickTime = 0
} else {
lastRightClickTime = now
}
})

// Link to transcribe if they have view permissions for it
if (CheckPermissions.checkViewAccess("LINE", "TEXT") || CheckPermissions.checkEditAccess("LINE", "TEXT")) {
let parsingRedirectButton = new OpenSeadragon.Button({
Expand Down Expand Up @@ -871,7 +883,8 @@ class AnnotoriousAnnotator extends HTMLElement {
this.#annotoriousInstance.setAnnotations(allAnnotations, false)
this.#annotoriousContainer.style.backgroundImage = "none"
this.shadowRoot.getElementById("tools-container").style.display = "block"
this.shadowRoot.querySelector("#autoParseBtn").style.display = "block"
// TODO This needs testing before we are ready for users to use it.
// this.shadowRoot.querySelector("#autoParseBtn").style.display = "block"
const pageSelector = this.shadowRoot.querySelector("tpen-page-selector")
if (pageSelector) {
pageSelector.style.display = "block"
Expand Down
2 changes: 1 addition & 1 deletion components/annotorious-annotator/plain.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* A plain Annotorious Annotator that can draw Rectangles. It is able to draw Polygons, but this ability is not exposed to the user.
* It assigns all Annotations to the provided AnnotationPage (does not make or track more than one page at a time)
*
* It is exposed to the user at /interfaces/annotator/index.html and so is our 'default annotator'.
* It is exposed to the user at /interfaces/annotator/index.html.
*
* The Annotation generation UI is powered by Annotorious. The TPEN3 team hereby acknowledges
* and thanks the Annotorious development team for this open source software.
Expand Down
3 changes: 2 additions & 1 deletion components/column-selector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export default class ColumnSelector extends HTMLElement {
return { ...col, label: isAuto ? `Unnamed ${i + 1}` : col.label }
})

this.#page = await vault.get(pageId, 'annotationpage', true)
this.#page = await vault.getWithFallback(pageId, 'annotationpage', TPEN.activeProject?.manifest, true)
if (!this.#page) return
const { orderedItems, columnsInPage, allColumnLines } = orderPageItemsByColumns(
{ columns: this.columns, items: page?.items },
this.#page
Expand Down
34 changes: 13 additions & 21 deletions components/continue-working/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import TPEN from '../../api/TPEN.js'
import vault from '../../js/vault.js'
import { stringFromDate } from '/js/utils.js'
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'

Expand Down Expand Up @@ -172,31 +173,22 @@ class ContinueWorking extends HTMLElement {
async getProjectThumbnail(project, annotationPageId) {
try {
if (!annotationPageId) return this.generateProjectPlaceholder(project)
const annotationPage = await fetch(`${TPEN.servicesURL}/project/${project._id}/page/${annotationPageId}`).then(r => r.json())
const annotationPage = await vault.get(
`${TPEN.servicesURL}/project/${project._id}/page/${annotationPageId}`,
'annotationpage',
true
)
if (!annotationPage) return this.generateProjectPlaceholder(project)
const canvasId = annotationPage.target
if (!canvasId) return this.generateProjectPlaceholder(project)

let canvas, isV3
try {
canvas = await fetch(canvasId).then(r => r.json())
const context = canvas['@context']
isV3 = Array.isArray(context)
? context.some(ctx => typeof ctx === 'string' && ctx.includes('iiif.io/api/presentation/3'))
: typeof context === 'string' && context.includes('iiif.io/api/presentation/3')
} catch {
// Fetch manifest
const manifestUrl = project.manifest?.[0]
if (!manifestUrl) return this.generateProjectPlaceholder(project)

const manifest = await fetch(manifestUrl).then(r => r.json())
const context = manifest['@context']
isV3 = Array.isArray(context)
? context.some(ctx => typeof ctx === 'string' && ctx.includes('iiif.io/api/presentation/3'))
: typeof context === 'string' && context.includes('iiif.io/api/presentation/3')
const canvases = isV3 ? manifest.items : manifest.sequences?.[0]?.canvases
canvas = canvases?.find(c => (isV3 ? c.id : c['@id']) === canvasId)
if (!canvas) return this.generateProjectPlaceholder(project)
}
canvas = await vault.getWithFallback(canvasId, 'canvas', project.manifest)

if (!canvas) return this.generateProjectPlaceholder(project)

// Structure-based detection
isV3 = Array.isArray(canvas.items) || canvas.type === "Canvas"

// Get thumbnail from canvas
let thumbnailUrl = canvas.thumbnail?.id ?? canvas.thumbnail?.['@id'] ?? canvas.thumbnail
Expand Down
35 changes: 13 additions & 22 deletions components/create-column/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import TPEN from "../../api/TPEN.js"
import CheckPermissions from "../check-permissions/checkPermissions.js"
import { onProjectReady } from "../../utilities/projectReady.js"
import { CleanupRegistry } from "../../utilities/CleanupRegistry.js"
import vault from '../../js/vault.js'

/**
* TpenCreateColumn - Interface for creating and managing columns on annotation pages.
Expand Down Expand Up @@ -563,27 +564,14 @@ class TpenCreateColumn extends HTMLElement {
return { x: xywh[0], y: xywh[1], w: xywh[2], h: xywh[3] }
}

isValidUrl(str) {
try {
new URL(str)
return true
} catch {
return false
}
}

async getSpecificTypeData(type) {
if (!type) throw new Error("No IIIF resource provided")
if (typeof type === "string" && this.isValidUrl(type)) {
const res = await fetch(type, { cache: "no-store" })
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`)
return await res.json()
}
}

async fetchPageViewerData(pageID = null) {
const annotationPageData = pageID ? await this.getSpecificTypeData(pageID) : null
const canvasData = await this.getSpecificTypeData(annotationPageData.target)
if (!pageID) throw new Error("No page ID provided")
const annotationPageData = await vault.getWithFallback(pageID, 'annotationpage', TPEN.activeProject?.manifest, true)
if (!annotationPageData) throw new Error("Failed to load annotation page")
const canvasData = await vault.getWithFallback(
annotationPageData.target, 'canvas', TPEN.activeProject?.manifest
)
if (!canvasData) throw new Error("Failed to load canvas data")
return await this.processDirectCanvasData(canvasData, annotationPageData)
}

Expand All @@ -597,8 +585,11 @@ class TpenCreateColumn extends HTMLElement {
if (!annotationPageData?.items) return []
const results = await Promise.all(annotationPageData.items.map(async anno => {
try {
const res = await fetch(anno.id, { cache: "no-store" })
const data = await res.json()
let data = anno
if (!data?.target) {
data = await vault.get(anno.id ?? anno, 'annotation', true)
}
if (!data) return null
return { target: data?.target?.selector?.value ?? data?.target, lineId: data?.id }
} catch { return null }
}))
Expand Down
2 changes: 2 additions & 0 deletions components/default-transcribe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* TpenTranscriptionElement - Default transcription view component.
* Displays line text and images for an annotation page.
* @element tpen-transcription
*
* @deprecated in favor of tpen-simple-transcription.
*/
import { userMessage, encodeContentState } from "../iiif-tools/index.js"
import "../line-image/index.js"
Expand Down
2 changes: 2 additions & 0 deletions components/legacy-annotator/plain.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*
* It is exposed to the user through /interfaces/annotator/legacy.html
* @element tpen-legacy-annotator
*
* @deprecated in favor of tpen-plain-annotator.
*/

import { eventDispatcher } from '../../api/events.js'
Expand Down
Loading
Loading