feat(typescript): add core infrastructure modules

- Add centralized API type definitions (types/api.d.ts)
  - Interfaces for all API responses matching Go handler JSON
  - Snake_case field names matching actual API responses
  - Source file references in comments for verification

- Add API client module (api.ts)
  - Procedural get/post/put/delete functions
  - Automatic auth header injection
  - Exported to window for cross-module access

- Add DOM utilities (dom.ts)
  - escapeHtml for safe HTML rendering
  - querySelector wrappers with null checks
  - Element creation helpers

- Add event delegation helpers (events.ts)
  - Reusable event delegation pattern
  - Data attribute selectors for dynamic content

- Add localStorage wrapper (storage.ts)
  - Type-safe token management
  - Theme persistence helpers
This commit is contained in:
2026-02-18 16:40:29 -05:00
parent 364de1ee93
commit 60c5a093b5
7 changed files with 751 additions and 2447 deletions
File diff suppressed because it is too large Load Diff
@@ -1,781 +0,0 @@
# TypeScript Conversion Plan Verification Checklist
Use this checklist to comprehensively audit the TypeScript Conversion Plan in a single pass. Each item includes verification steps to confirm accuracy.
---
## 1. Type Definition Verification
### 1.1 Verify All Types Match Actual API Responses
**For each type definition in `web/src/types/api.d.ts` (planned):**
- [ ] Locate the API endpoint in `internal/handlers/*.go`
- [ ] Check what the endpoint **actually returns** (return statement)
- [ ] Determine if it returns:
- Handler-defined struct (e.g., `handlers.BookInfo`)
- Database row (e.g., `database.SearchMediaItemsRow`)
- Map/slice of either
- [ ] Find the source type definition:
- Handler structs: `internal/handlers/*.go`
- Database rows: `internal/database/queries.sql.go`
- [ ] Verify all JSON tags match TypeScript interface fields (snake_case)
- [ ] Map pgtype fields to TypeScript types:
- `pgtype.Text``string | undefined`
- `pgtype.UUID``string`
- `pgtype.Timestamp``string` (ISO datetime)
- `pgtype.Numeric``number` or `string`
- `pgtype.Bool``boolean`
- `[]string``string[]`
**Common Pitfalls:**
- Handler structs may be defined but unused (e.g., `search.go`'s `MediaItemSummary`)
- Endpoints may return database rows directly, not handler structs
- JSON tags may use snake_case while Go fields use PascalCase
- Arrays vs single values (e.g., `author` string vs `authors` array)
**Verification Commands:**
```bash
# Find endpoint implementation
rg "func.*SearchMediaItems" internal/handlers/
# Check return type
rg -A 10 "func.*SearchMediaItems" internal/handlers/media.go | grep "return c.JSON"
# Find type definition
rg "type SearchMediaItemsRow struct" internal/database/queries.sql.go
# Check JSON tags
rg 'json:"' internal/database/queries.sql.go | grep "SearchMediaItemsRow"
```
### 1.2 Verify No Duplicate/Conflicting Type Definitions
- [ ] No two TypeScript interfaces describe the same API response
- [ ] No interface fields contradict actual JSON response
- [ ] No unused handler structs are referenced in type comments
- [ ] All inline types in `.ts` files could move to `types/api.d.ts`
---
## 2. API Contract Verification
### 2.1 Verify All API Endpoints Referenced in Plan Exist
**For each endpoint mentioned:**
- [ ] Endpoint exists in `internal/handlers/*.go`
- [ ] Route is registered in routing code
- [ ] HTTP method matches (GET/POST/PUT/DELETE)
- [ ] Response format matches plan's type definitions
- [ ] Authentication requirements match plan assumptions
**Check Commands:**
```bash
# Find endpoint definition
rg "POST.*collections.*rules" internal/handlers/
# Check route registration
rg "collections.*rules" cmd/server/ or internal/router/
```
### 2.2 Verify Authentication Patterns
- [ ] All API endpoints that require auth are documented
- [ ] Token storage approach matches (`localStorage.getItem('token')`)
- [ ] Auth header format consistent (`Bearer ${token}`)
- [ ] 401 handling documented (token clearing, redirect)
### 2.3 Verify Error Response Formats
- [ ] Error responses use consistent structure (`{"error": "message"}`)
- [ ] Toast integration documented for all error cases
- [ ] Network error handling documented
- [ ] Validation error handling documented (400 responses)
---
## 3. Cross-Reference Verification
### 3.1 Template-Handler Type Sharing
**For each template that imports handlers:**
- [ ] Template imports `internal/handlers` package
- [ ] Template uses handler types for SSR data (e.g., `handlers.BookInfo`)
- [ ] TypeScript interfaces match the same handler JSON responses
- [ ] No duplicate type definitions between handlers and templates
- [ ] Template-only types are clearly marked (e.g., `PageData`, `UnsafeHTML`)
**Verification:**
```bash
# Find templates importing handlers
rg 'import.*handlers' templates/*.templ
# Check handler type usage in templates
rg 'handlers\.(CollectionData|BookInfo|UserProfile)' templates/*.templ
```
### 3.2 Existing TypeScript Module Patterns
**Verify all existing `.ts` modules follow documented patterns:**
- [ ] Procedural style (no classes, no `this`)
- [ ] Functions exported to `window` object
- [ ] No ES module imports/exports (browser globals)
- [ ] Proper type annotations (`import type` for type-only imports)
- [ ] Consistent error handling with toast integration
**Check existing modules:**
- `web/src/toast.ts`
- `web/src/theme.ts`
- `web/src/header.ts`
- `web/src/device-management.ts`
### 3.3 JavaScript to TypeScript Mapping
**For each inline script in templates:**
- [ ] Identify all `<script>` blocks in templates
- [ ] Count total lines of inline JavaScript
- [ ] Verify plan's line count estimates are accurate
- [ ] Check for duplicate functionality (e.g., login.templ theme code vs theme.ts)
- [ ] Verify no critical functionality is missed in conversion plan
**Verification Commands:**
```bash
# Count script blocks
rg '<script>' templates/*.templ | wc -l
# Count inline JS lines
rg '<script>' templates/*.templ -A 100 | grep -v '^--$' | wc -l
# Find duplicate functions
rg "function applyTheme" templates/*.templ web/src/*.ts
```
---
## 4. Architecture Verification
### 4.1 Verify Hybrid SSR Architecture Preserved
**For each feature conversion:**
- [ ] Initial page load uses SSR (Go templates with data)
- [ ] TypeScript only handles CRUD operations (create, update, delete)
- [ ] No new HTML endpoints needed (use existing `/api/*` endpoints)
- [ ] Service layer remains source of truth
- [ ] No handler duplication between SSR and API
**Check:**
```bash
# Verify templates use handler data
rg 'CollectionData\|BookInfo\|UserProfile' templates/*.templ
# Verify API endpoints exist
rg 'GET.*collections\|POST.*collections' internal/handlers/
```
### 4.2 Verify Progressive Enhancement
**For each form/user flow:**
- [ ] Form has `action` and `method` attributes
- [ ] Form submits correctly without JavaScript (test in browser)
- [ ] HTMX attributes enhance but don't replace standard behavior
- [ ] Critical paths work without JS (login, create, update, delete)
- [ ] TypeScript interception is optional enhancement, not requirement
**Test Procedure:**
1. Open DevTools → Disable JavaScript
2. Navigate to page
3. Verify form submits with full page reload
4. Re-enable JavaScript
5. Verify enhanced behavior works
### 4.3 Verify No Backend Modifications for Frontend Tasks
**For each TypeScript module:**
- [ ] Only uses existing API endpoints
- [ ] No new handler functions needed
- [ ] No database schema changes needed
- [ ] No migration files needed
- [ ] All functionality achievable with current API
**Red Flags:**
- Plan mentions "need to add endpoint for..."
- TypeScript code requires new backend structure
- Data transformation needed in handler
---
## 5. Build & Deployment Verification
### 5.1 Verify TypeScript Configuration
- [ ] `tsconfig.json` exists and is correctly configured
- [ ] `module: "none"` for browser globals
- [ ] `outDir: "./web/static"` for flat output
- [ ] `rootDir: "./web/src"` for source organization
- [ ] `strict: true` for type safety
- [ ] `.d.ts` files don't generate JS output
- [ ] Source maps configured appropriately
**Check:**
```bash
# Verify tsconfig.json
cat tsconfig.json
# Test compilation
npm run build:ts
# Check output
ls -la web/static/*.js
```
### 5.2 Verify Build Scripts
- [ ] `package.json` has correct build scripts
- [ ] `npm run build:ts` compiles TypeScript
- [ ] `npm run build:ts:watch` for development
- [ ] No additional bundlers needed (webpack, vite, etc.)
- [ ] Build output matches plan's expectations
**Check:**
```bash
# Verify scripts
cat package.json | grep -A 5 '"scripts"'
# Test build
npm run build:ts
# Verify output structure
ls -la web/static/
```
### 5.3 Verify Docker/Podman Integration
- [ ] TypeScript compilation happens during Docker build
- [ ] No local binary builds needed
- [ `.gitignore` excludes `.js` files but includes `.ts` files
- [ ] Production build includes minified CSS
**Check:**
```bash
# Check Dockerfile
rg "npm run build" Dockerfile
# Check .gitignore
rg "\.js$" .gitignore
```
---
## 6. Phase Dependency Verification
### 6.1 Verify Phase Ordering
**For each phase:**
- [ ] All dependencies are completed in previous phases
- [ ] No circular dependencies between phases
- [ ] Core utilities (Phase 2) before feature modules (Phases 3-5)
- [ ] Standalone files (Phase 1) before inline conversion
- [ ] Template cleanup (Phase 6) after all conversions complete
**Check dependency graph:**
```
Phase 1 (search.js) → No dependencies ✓
Phase 2 (utils) → No dependencies ✓
Phase 3 (low complexity) → Needs Phase 2 ✓
Phase 4 (medium complexity) → Needs Phase 2, 3 ✓
Phase 5 (high complexity) → Needs Phase 4 ✓
Phase 6 (cleanup) → Needs all phases ✓
```
### 6.2 Verify Phase Completeness
**For each phase:**
- [ ] All deliverables are listed
- [ ] All files to create/delete/update are specified
- [ ] Build verification steps are included
- [ ] Testing steps are documented
- [ ] Rollback strategy is mentioned
### 6.3 Verify Timeline Estimates
**For each phase:**
- [ ] Effort estimates are realistic
- [ ] Line counts match actual code
- [ ] Total duration is achievable (16-21 days)
- [ ] No phase exceeds 6 days (break down if needed)
---
## 7. Code Style Verification
### 7.1 Verify Procedural Style Compliance
**Check all proposed code examples:**
- [ ] No classes used
- [ ] No `this` references
- [ ] No inheritance
- [ ] Functions are standalone, not methods
- [ ] State is in variables/parameters, not properties
- [ ] Event delegation used where appropriate
**Examples should follow:**
```typescript
// ✅ GOOD: Procedural
function deleteRule(ruleId: string): void {
// implementation
}
window.deleteRule = deleteRule;
// ❌ BAD: OOP
class RuleManager {
deleteRule(ruleId: string): void {
// implementation
}
}
```
### 7.2 Verify Event Handler Patterns
**Check appropriate use of:**
- [ ] `onclick` for simple static content
- [ ] `data-action` + event delegation for dynamic content
- [ ] Form interception for progressive enhancement
- [ ] No over-engineering (not everything needs data-action)
**Plan should include:**
- When to use each pattern
- Examples of each pattern
- Rationale for pattern selection
### 7.3 Verify TailwindCSS Usage
- [ ] No custom CSS in TypeScript files
- [ ] All styling uses Tailwind classes
- [ ] No inline `style` attributes except for dynamic values
- [ ] Theme variables used appropriately (`var(--bg-primary)`, etc.)
---
## 8. Documentation Verification
### 8.1 Verify Type Definition Documentation
**For each type in `web/src/types/api.d.ts`:**
- [ ] Comment references source file (database or handler)
- [ ] Comment includes line numbers or struct name
- [ ] Fields have inline comments if purpose unclear
- [ ] Complex types have usage examples
**Example:**
```typescript
// Matches database.SearchMediaItemsRow from /api/media-items/search
// Source: internal/database/queries.sql.go:SearchMediaItemsRow
// Used in: search.ts
export interface MediaItemSummary {
id: string;
title: string;
// ...
}
```
### 8.2 Verify Code Example Accuracy
**For each code example in plan:**
- [ ] Code actually compiles/runs
- [ ] Imports are correct for module system
- [ ] Type annotations match actual types
- [ ] Variable names match plan's conventions
- [ ] No syntax errors
**Test by:**
- Copying example to actual `.ts` file
- Running `npm run build:ts`
- Checking for compilation errors
### 8.3 Verify Template Examples
**For each template example:**
- [ ] Valid Go template syntax (`templ` not `html`)
- [ ] Correct handler type references
- [ ] Script tags use correct paths
- [ ] No inline JavaScript (after conversion)
- [ ] HTMX attributes are correct
---
## 9. Template Integration Verification
### 9.1 Verify Template Cleanup Tasks
**For each template mentioned in Phase 6:**
- [ ] Current template file exists
- [ ] Line numbers for duplicate code are accurate
- [ ] Removal won't break functionality
- [ ] Replacement TypeScript module exists
- [ ] Script tag added to template `<head>`
**Verify specific tasks:**
- [ ] login.templ lines 62-88 (duplicate theme code)
- [ ] Any other duplicate function calls
- [ ] Any unused inline script blocks
**Check:**
```bash
# Find duplicate theme code
rg "function applyTheme" templates/login.templ web/src/theme.ts
# Count script blocks per template
rg '<script>' templates/login.templ -c
```
### 9.2 Verify SSR Data Preservation
**For each template conversion:**
- [ ] Initial data still rendered server-side
- [ ] Template still uses handler types (e.g., `handlers.CollectionData`)
- [ ] No client-side data fetching for initial load
- [ ] TypeScript only enhances, doesn't replace SSR
**Example check:**
```go
// ✅ CORRECT: Template receives data from handler
templ Collections(collections []handlers.CollectionData)
// ❌ WRONG: Template fetches data client-side
// This would break progressive enhancement
```
---
## 10. Progressive Enhancement Verification
### 10.1 Verify Critical User Flows
**For each critical flow (login, CRUD operations):**
- [ ] Works without JavaScript (standard form submission)
- [ ] Enhanced with JavaScript (AJAX, toasts, etc.)
- [ ] No JavaScript required for basic functionality
- [ ] Graceful degradation for mobile/slow connections
**Test procedure:**
1. Disable JavaScript in browser
2. Attempt each critical flow
3. Verify functionality works (with page reloads)
4. Enable JavaScript
5. Verify enhanced experience
### 10.2 Verify HTMX Usage
**For each HTMX form:**
- [ ] Has `action` and `method` attributes (fallback)
- [ ] HTMX attributes (`hx-post`, `hx-target`, etc.) enhance behavior
- [ ] No TypeScript interception needed (HTMX handles it)
- [ ] Works without JavaScript enabled
**Check:**
```bash
# Find HTMX forms
rg 'hx-post|hx-get' templates/*.templ
# Verify standard attributes exist
rg '<form' templates/*.templ -A 3 | grep "action="
```
### 10.3 Verify Error Handling
**For error scenarios:**
- [ ] Network errors show toast notification
- [ ] 401 errors clear tokens and redirect
- [ ] 404/500 errors display user-friendly messages
- [ ] Validation errors (400) show field-specific feedback
- [ ] No console errors in normal operation
---
## 11. Missing Information Detection
### 11.1 Check for Unaddressed Template Scripts
**For each template with `<script>` blocks:**
- [ ] Script is mentioned in conversion plan
- [ ] Conversion phase is specified
- [ ] TypeScript module is planned
- [ ] No inline JavaScript is overlooked
**Find all scripts:**
```bash
# List all templates with scripts
rg '<script>' templates/*.templ -l
# Count scripts per template
rg '<script>' templates/*.templ -c
```
### 11.2 Check for Missing API Endpoints
**For each TypeScript operation mentioned:**
- [ ] API endpoint exists
- [ ] Endpoint is documented in plan
- [ ] Type definition matches endpoint response
- [ ] Authentication requirements are clear
**Find all fetch calls:**
```bash
# Find fetch in plan
rg "fetch\(" TYPESCRIPT_CONVERSION_PLAN.md | grep "/api/"
```
### 11.3 Check for Missing Build Steps
**Verify plan includes:**
- [ ] TypeScript compilation command
- [ ] Watch mode command for development
- [ ] Production build steps
- [ ] Docker integration
- [ ] Verification steps after build
---
## 12. Contradiction Detection
### 12.1 Check for Internal Contradictions
**Search for conflicting statements:**
- [ ] Architecture described differently in different sections
- [ ] Type definitions that contradict each other
- [ ] Phase ordering that conflicts with dependencies
- [ ] Code style examples that violate guidelines
**Common contradictions:**
- "No classes" but example shows class
- "Use snake_case" but example uses camelCase
- "Progressive enhancement required" but form lacks action attribute
- "Don't modify backend" but plan suggests new endpoint
### 12.2 Check for Contradictions with Codebase
**Verify plan matches reality:**
- [ ] Existing TypeScript files match plan's patterns
- [ ] Build configuration matches plan's description
- [ ] Template structure matches plan's assumptions
- [ ] API endpoints match plan's documentation
**Cross-check:**
```bash
# Verify existing TS patterns
rg "class " web/src/*.ts
# Should return nothing if plan is correct
# Verify build config
rg "module.*none" tsconfig.json
# Should match plan
```
### 12.3 Check for Contradictions with Guidelines
**Compare with PROJECT_GUIDELINES.md:**
- [ ] No OOP in TypeScript (procedural only)
- [ ] Use TailwindCSS (no custom CSS)
- [ ] Progressive enhancement required
- [ ] SSR for initial loads
- [ ] Service layer as source of truth
- [ ] No handler duplication
---
## 13. Realism Verification
### 13.1 Verify Line Count Estimates
**For each template/script mentioned:**
- [ ] Count actual lines in file
- [ ] Compare to plan's estimate
- [ ] Estimate is within ±20% of actual
**Count lines:**
```bash
# Count inline JS in template
rg -A 1000 '<script>' templates/collection_rules.templ | wc -l
# Count total template lines
wc -l templates/*.templ
```
### 13.2 Verify Phase Duration Estimates
**For each phase:**
- [ ] Effort matches complexity (low = 1-2 days, medium = 3-4, high = 5-6)
- [ ] Total lines to convert justify duration
- [ ] Phase dependencies don't create bottlenecks
- [ ] Parallel work is possible where stated
### 13.3 Verify Technical Feasibility
**Check for over-ambitious goals:**
- [ ] No entirely new architecture proposed
- [ ] No unproven technologies
- [ ] No breaking changes to existing systems
- [ ] Rollback is possible if conversion fails
- [ ] Testing is feasible within timeline
---
## 14. Testing Verification
### 14.1 Verify Testing Strategy
**Plan should include:**
- [ ] Unit testing approach (if applicable)
- [ ] Integration testing approach
- [ ] Manual testing checklist
- [ ] Browser testing matrix
- [ ] Progressive enhancement testing
- [ ] Performance testing (bundle sizes)
### 14.2 Verify Test Coverage
**For each converted feature:**
- [ ] Test steps are documented
- [ ] Success criteria are defined
- [ ] Edge cases are considered
- [ ] Error cases are tested
- [ ] Progressive enhancement is verified
### 14.3 Verify Rollback Strategy
**For each phase:**
- [ ] Git branch strategy is defined
- [ ] Quick rollback procedure is documented
- [ ] Feature flags or gradual rollout possible
- [ ] No destructive changes until verified
---
## 15. Success Criteria Verification
### 15.1 Verify All Success Criteria are Measurable
**For each success criterion:**
- [ ] Can be verified with yes/no check
- [ ] Has clear definition of "done"
- [ ] Can be tested automatically or manually
- [ ] Is realistic given timeline
### 15.2 Verify Success Criteria Match Goals
**Check criteria align with:**
- [ ] TypeScript conversion (all JS converted)
- [ ] Type safety (no `any` except legacy)
- [ ] Architecture preservation (SSR, progressive enhancement)
- [ ] Code quality (procedural style, no OOP)
- [ ] Functionality (no regressions)
### 15.3 Verify Success Criteria are Complete
**Ensure criteria cover:**
- [ ] Code conversion completeness
- [ ] Type safety standards
- [ ] Compilation/build success
- [ ] Testing completion
- [ ] Documentation updates
- [ ] No regressions
- [ ] Performance standards
---
## Summary Checklist
Before approving the TypeScript conversion plan, verify:
### Critical (Must Pass)
- [ ] All type definitions match actual API responses (check database layer)
- [ ] No unused handler structs referenced in type definitions
- [ ] All API endpoints referenced actually exist
- [ ] Progressive enhancement maintained for all critical flows
- [ ] No backend modifications required for frontend tasks
- [ ] Build configuration is correct and tested
- [ ] Phase dependencies are acyclic and logical
- [ ] All inline JavaScript is accounted for in conversion plan
### Important (Should Pass)
- [ ] Code style examples match documented guidelines
- [ ] Template cleanup tasks have accurate line numbers
- [ ] Timeline estimates are realistic
- [ ] Testing strategy is comprehensive
- [ ] Rollback strategy is clear
- [ ] No internal contradictions in plan
- [ ] No contradictions with codebase reality
- [ ] No contradictions with PROJECT_GUIDELINES.md
### Nice to Have
- [ ] Documentation is clear and complete
- [ ] Examples are accurate and helpful
- [ ] Common pitfalls are documented
- [ ] Verification commands are provided
- [ ] Success criteria are measurable
- [ ] Plan is easy to follow for implementers
---
## Usage Instructions
1. **Before Starting Review:** Read the entire TYPESCRIPT_CONVERSION_PLAN.md
2. **During Review:** Go through each section of this checklist systematically
3. **For Each Item:** Run the provided verification commands
4. **Document Findings:** Note any issues found with file/line references
5. **Categorize Issues:** Mark as Critical, Important, or Nice to Have
6. **Verify Fixes:** After fixing issues, re-run relevant checklist sections
## Common Issues Found
1. **Type Mismatches:** Handler structs defined but not used by endpoints
2. **Missing Fields:** TypeScript interfaces missing fields that API returns
3. **Wrong Field Types:** `string` vs `string[]`, `author` vs `authors`
4. **Duplicate Code:** Inline JavaScript duplicating existing TypeScript
5. **Overlooked Scripts:** `<script>` blocks not mentioned in plan
6. **Missing Endpoints:** API references that don't exist
7. **Broken Dependencies:** Phase B depends on Phase A, but ordered incorrectly
8. **Contradictions:** Plan says one thing, examples show another
9. **Unrealistic Estimates:** 500 lines of complex code in 1 day
10. **Missing Testing:** No way to verify conversion worked
---
*Created: 2025-02-18*
*Purpose: Comprehensive verification of TypeScript conversion plan*
*Version: 1.0*
+101
View File
@@ -0,0 +1,101 @@
function getAuthHeader(): string {
const token = localStorage.getItem('token');
return token ? `Bearer ${token}` : '';
}
async function apiGet(url: string): Promise<Response> {
return fetch(`/api${url}`, {
headers: {
'Authorization': getAuthHeader(),
'Content-Type': 'application/json'
}
});
}
async function apiPost(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: 'POST',
headers: {
'Authorization': getAuthHeader(),
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
}
async function apiPut(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: 'PUT',
headers: {
'Authorization': getAuthHeader(),
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
}
async function apiDelete(url: string): Promise<Response> {
return fetch(`/api${url}`, {
method: 'DELETE',
headers: {
'Authorization': getAuthHeader()
}
});
}
async function apiPatch(url: string, data?: unknown): Promise<Response> {
return fetch(`/api${url}`, {
method: 'PATCH',
headers: {
'Authorization': getAuthHeader(),
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
return response.json();
}
async function handleVoidResponse(response: Response): Promise<void> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
}
function handleError(error: unknown, context: string): void {
console.error(`${context}:`, error);
const message = error instanceof Error ? error.message : 'An unexpected error occurred';
if ((window as any).showToast?.error) {
(window as any).showToast.error(message);
}
}
(window as any).api = {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse,
handleVoidResponse,
handleError
};
export {
getAuthHeader,
apiGet,
apiPost,
apiPut,
apiDelete,
apiPatch,
handleResponse,
handleVoidResponse,
handleError
};
+137
View File
@@ -0,0 +1,137 @@
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function querySelector<T extends Element>(selector: string): T | null {
return document.querySelector<T>(selector);
}
function querySelectorAll<T extends Element>(selector: string): NodeListOf<T> {
return document.querySelectorAll<T>(selector);
}
function getElementById<T extends HTMLElement>(id: string): T | null {
return document.getElementById(id) as T | null;
}
function createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
attributes?: Record<string, string>,
children?: (string | Node)[]
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName);
if (attributes) {
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'dataset') {
Object.entries(JSON.parse(value)).forEach(([dataKey, dataValue]) => {
element.dataset[dataKey] = String(dataValue);
});
} else {
element.setAttribute(key, value);
}
});
}
if (children) {
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
}
return element;
}
function showElement(element: HTMLElement | null): void {
if (element) {
element.classList.remove('hidden');
}
}
function hideElement(element: HTMLElement | null): void {
if (element) {
element.classList.add('hidden');
}
}
function toggleElement(element: HTMLElement | null): void {
if (element) {
element.classList.toggle('hidden');
}
}
function setTextContent(element: HTMLElement | null, text: string): void {
if (element) {
element.textContent = text;
}
}
function setInnerHTML(element: HTMLElement | null, html: string): void {
if (element) {
element.innerHTML = html;
}
}
function addClass(element: HTMLElement | null, className: string): void {
if (element) {
element.classList.add(className);
}
}
function removeClass(element: HTMLElement | null, className: string): void {
if (element) {
element.classList.remove(className);
}
}
function toggleClass(element: HTMLElement | null, className: string): void {
if (element) {
element.classList.toggle(className);
}
}
function hasClass(element: HTMLElement | null, className: string): boolean {
return element ? element.classList.contains(className) : false;
}
(window as any).dom = {
escapeHtml,
querySelector,
querySelectorAll,
getElementById,
createElement,
showElement,
hideElement,
toggleElement,
setTextContent,
setInnerHTML,
addClass,
removeClass,
toggleClass,
hasClass
};
export {
escapeHtml,
querySelector,
querySelectorAll,
getElementById,
createElement,
showElement,
hideElement,
toggleElement,
setTextContent,
setInnerHTML,
addClass,
removeClass,
toggleClass,
hasClass
};
+117
View File
@@ -0,0 +1,117 @@
function onDelegatedClick(selector: string, handler: (element: HTMLElement, event: MouseEvent) => void): void {
document.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
const element = target.closest(selector) as HTMLElement | null;
if (element) {
handler(element, event);
}
});
}
function onDelegatedSubmit(selector: string, handler: (form: HTMLFormElement, event: Event) => void): void {
document.addEventListener('submit', (event: Event) => {
const target = event.target as HTMLElement;
const form = target.closest(selector) as HTMLFormElement | null;
if (form) {
handler(form, event);
}
});
}
function onDelegatedChange(selector: string, handler: (element: HTMLElement, event: Event) => void): void {
document.addEventListener('change', (event: Event) => {
const target = event.target as HTMLElement;
const element = target.closest(selector) as HTMLElement | null;
if (element) {
handler(element, event);
}
});
}
function onDelegatedKeydown(selector: string, handler: (element: HTMLElement, event: KeyboardEvent) => void): void {
document.addEventListener('keydown', (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
const element = target.closest(selector) as HTMLElement | null;
if (element) {
handler(element, event);
}
});
}
function getDataAttribute(element: HTMLElement, name: string): string | undefined {
return element.dataset[name];
}
function setDataAttribute(element: HTMLElement, name: string, value: string): void {
element.dataset[name] = value;
}
function onClick(element: HTMLElement | null, handler: (event: MouseEvent) => void): void {
if (element) {
element.addEventListener('click', handler);
}
}
function onSubmit(element: HTMLFormElement | null, handler: (event: Event) => void): void {
if (element) {
element.addEventListener('submit', handler);
}
}
function onChange(element: HTMLElement | null, handler: (event: Event) => void): void {
if (element) {
element.addEventListener('change', handler);
}
}
function onKeydown(element: HTMLElement | null, handler: (event: KeyboardEvent) => void): void {
if (element) {
element.addEventListener('keydown', handler);
}
}
function onInput(element: HTMLElement | null, handler: (event: Event) => void): void {
if (element) {
element.addEventListener('input', handler);
}
}
function preventDefault(event: Event): void {
event.preventDefault();
}
function stopPropagation(event: Event): void {
event.stopPropagation();
}
(window as any).events = {
onDelegatedClick,
onDelegatedSubmit,
onDelegatedChange,
onDelegatedKeydown,
getDataAttribute,
setDataAttribute,
onClick,
onSubmit,
onChange,
onKeydown,
onInput,
preventDefault,
stopPropagation
};
export {
onDelegatedClick,
onDelegatedSubmit,
onDelegatedChange,
onDelegatedKeydown,
getDataAttribute,
setDataAttribute,
onClick,
onSubmit,
onChange,
onKeydown,
onInput,
preventDefault,
stopPropagation
};
+83
View File
@@ -0,0 +1,83 @@
function getToken(): string | null {
return localStorage.getItem('token');
}
function setToken(token: string): void {
localStorage.setItem('token', token);
}
function removeToken(): void {
localStorage.removeItem('token');
}
function getRefreshToken(): string | null {
return localStorage.getItem('refresh_token');
}
function setRefreshToken(token: string): void {
localStorage.setItem('refresh_token', token);
}
function removeRefreshToken(): void {
localStorage.removeItem('refresh_token');
}
function getTheme(): string {
return localStorage.getItem('theme') || 'tokyo-night';
}
function setTheme(theme: string): void {
localStorage.setItem('theme', theme);
}
function getSelectedLibrary(): string | null {
return localStorage.getItem('selectedLibrary');
}
function setSelectedLibrary(libraryId: string): void {
localStorage.setItem('selectedLibrary', libraryId);
}
function getSelectedBook(): string | null {
return localStorage.getItem('selectedBook');
}
function setSelectedBook(bookId: string): void {
localStorage.setItem('selectedBook', bookId);
}
function clearAll(): void {
localStorage.clear();
}
(window as any).storage = {
getToken,
setToken,
removeToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
getTheme,
setTheme,
getSelectedLibrary,
setSelectedLibrary,
getSelectedBook,
setSelectedBook,
clearAll
};
export {
getToken,
setToken,
removeToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
getTheme,
setTheme,
getSelectedLibrary,
setSelectedLibrary,
getSelectedBook,
setSelectedBook,
clearAll
};
+313
View File
@@ -0,0 +1,313 @@
// ============================================
// API Type Definitions
// ============================================
// These types match the JSON responses from /api/* endpoints.
// Source of truth: Check what the endpoint ACTUALLY returns:
// 1. Database layer: internal/database/queries.sql.go (SearchMediaItemsRow, etc.)
// 2. Handler structs: internal/handlers/*.go (check json:"..." tags)
// 3. Test by calling endpoint and inspecting JSON response
//
// When API contracts change:
// 1. Find the endpoint function in internal/handlers/*.go
// 2. Check what it returns (database row or struct)
// 3. Check the JSON tags: `json:"field_name"`
// 4. Map pgtype fields to TypeScript types:
// - pgtype.Text → string | undefined
// - pgtype.UUID → string
// - pgtype.Timestamp → string (ISO datetime)
// - pgtype.Numeric → number or string (for precision)
// 5. Update the interface below with snake_case field names
// 6. Run Bruno tests to verify
// ============================================
// Matches database.SearchMediaItemsRow from /api/media-items/search
// Source: internal/database/queries.sql.go:6123-6170 SearchMediaItemsRow
// Endpoint: internal/handlers/media.go:SearchMediaItems()
// Note: internal/handlers/search.go has an unused MediaItemSummary - ignore it
// Used in: search.ts
export interface MediaItemSummary {
id: string;
library_id: string;
title: string;
author?: string;
isbn?: string;
description?: string;
file_path: string;
file_size?: number;
mime_type?: string;
cover_image_path?: string;
series?: string;
series_number?: number;
tags?: string[];
asin?: string;
date_published?: string;
publisher?: string;
contributors?: string[];
language?: string;
edition?: string;
page_count?: number;
genre?: string;
copyright_year?: number;
goodreads_id?: string;
openlibrary_id?: string;
google_books_id?: string;
added_by_admin_id?: string;
created_at: string;
updated_at: string;
format_group: string;
format_mimetype?: string;
is_reflowable?: boolean;
has_fixed_layout?: boolean;
total_characters?: number;
chapter_count?: number;
entitlement_id?: string;
revision_number?: number;
kobo_content_id?: string;
kobo_metadata?: string;
tags_search?: string[];
contributors_search?: string[];
file_sha256?: string;
opf_identifier?: string;
opf_uuid?: string;
hash_confidence?: string;
library_name: string;
library_type_name: string;
}
// Matches handlers.CollectionData / CollectionResponse JSON response
// Source: internal/handlers/collections.go:123-131 CollectionResponse
// Used in: collections.ts
export interface CollectionData {
id: string;
name: string;
description: string;
color: string;
icon: string;
auto_assign_rules?: unknown;
created_at: string;
}
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
// JSON tags: media_item_id, title, author, cover_image_path
// Used in: collections.templ (server-rendered), collections.ts
export interface BookInfo {
media_item_id: string;
title: string;
author: string;
cover_image_path: string;
}
// Matches handlers.UnlinkedBookData JSON response
// Used in: unlinked_books.ts, unlinked_books.templ
export interface UnlinkedBookData {
progress_id: string;
device_id: string;
device_name: string;
device_type: 'koreader' | 'kobo' | 'web';
title_from_device: string;
file_path: string;
sha256: string;
last_sync_time: string;
confidence_score: number;
potential_matches: PotentialMatchData[];
}
export interface PotentialMatchData {
media_item_id: string;
title: string;
author: string;
confidence: number;
cover_image_path?: string;
}
// Matches collection rule objects
// Used in: collection_rules.ts
export interface CollectionRule {
id: string;
field: 'genre' | 'series' | 'author' | 'language' | 'publisher' | 'copyright_year' | 'tags';
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'greater_than' | 'less_than';
value: string;
enabled: boolean;
priority: number;
}
// Matches API test rule responses
// Used in: collection_rules.ts (test results)
export interface TestRuleMatch {
title: string;
author: string;
cover_image_path?: string;
}
// Matches handlers.SearchResponse (internal/handlers/search.go)
export interface SearchResponse {
results: SearchBookResponse[];
total: number;
}
export interface SearchBookResponse {
id: string;
title: string;
authors: SearchAuthor[];
}
export interface SearchAuthor {
first_name: string;
last_name: string;
}
// Matches AuthResponse (internal/handlers/auth.go:59-65)
export interface AuthResponse {
access_token: string;
refresh_token?: string;
token_type: string;
expires_in: number;
user: UserProfile;
}
export interface UserProfile {
id: string;
email: string;
username: string;
first_name?: string;
last_name?: string;
role: string;
theme?: string;
}
// Matches handlers.ReadingStatsResponse (internal/handlers/analytics.go:26-35)
// Used in: analytics.ts
export interface ReadingStatsResponse {
total_books_read: number;
total_pages_read: number;
total_reading_time_minutes: number;
average_session_time_minutes: number;
longest_session_minutes: number;
most_active_day_of_week: string;
completion_rate: number;
daily_reading_minutes: DailyReading[];
}
export interface DailyReading {
date: string;
minutes: number;
pages: number;
}
// Matches handlers.DeviceUsageResponse (internal/handlers/analytics.go:43-45)
// Note: Response is wrapped: { devices: DeviceUsage[] }
// Used in: analytics.ts
export interface DeviceUsageResponse {
devices: DeviceUsage[];
}
export interface DeviceUsage {
device_id: string;
device_name: string;
device_type: string;
sync_count: number;
last_sync: string;
total_time_seconds: number;
total_time_minutes: number;
}
// Matches handlers.PopularBooksResponse (internal/handlers/analytics.go:57-59)
// Note: Response is wrapped: { books: PopularBook[] }
// Used in: analytics.ts
export interface PopularBooksResponse {
books: PopularBook[];
}
export interface PopularBook {
media_item_id: string;
title: string;
author: string;
read_count: number;
avg_completion: number;
last_read: string;
}
// Matches handlers.QueueItemResponse (internal/handlers/queue.go:35-51)
// Used in: queue.ts
export interface QueueItemResponse {
id: string;
device_id: string;
device_name: string;
device_type: string;
media_item_id?: string;
media_title?: string;
user_email: string;
sync_type: string;
priority: number;
attempts: number;
max_attempts: number;
status: string;
error_message?: string;
created_at: string;
processed_at?: string;
}
// Matches handlers.QueueStatsResponse (internal/handlers/queue.go:27-33)
// Used in: queue.ts
export interface QueueStatsResponse {
pending_count: number;
processing_count: number;
failed_count: number;
completed_count: number;
total_count: number;
}
// Matches handlers.ConflictDetailResponse (internal/handlers/conflicts.go:42-53)
// Used in: conflicts.ts
export interface ConflictDetailResponse {
id: string;
media_item_id: string;
media_item_title: string;
conflict_type: string;
conflict_data: Record<string, ConflictSourceData>;
resolution_status: string;
resolution_data?: Record<string, unknown>;
resolved_by?: string;
resolved_at?: string;
created_at: string;
}
// Matches handlers.ConflictSourceData (internal/handlers/conflicts.go:36-40)
export interface ConflictSourceData {
source: string;
timestamp: string;
data: Record<string, unknown>;
}
// Matches handlers.ConflictListResponse (internal/handlers/conflicts.go:55-59)
// Used in: conflicts.ts
export interface ConflictListResponse {
conflicts: ConflictDetailResponse[];
total: number;
unresolved: number;
}
// Matches handlers.ConflictResolveResponse (internal/handlers/conflicts.go:61-65)
// Used in: conflicts.ts
export interface ConflictResolveResponse {
conflict_resolved: boolean;
applied_to: Record<string, boolean>;
devices_synced: string[];
}
// Matches handlers.BulkResolveResponse (internal/handlers/conflicts.go:424-429)
// Used in: conflicts.ts
export interface BulkResolveResponse {
results: ConflictResult[];
total: number;
success: number;
failed: number;
}
// Matches handlers.ConflictResult (internal/handlers/conflicts.go:431-436)
export interface ConflictResult {
conflict_id: string;
status: string;
error?: string;
winner?: string;
}