docs(typescript): add TypeScript conversion plan for inline JavaScript
- Create comprehensive plan to convert ~5,500 lines of inline JS to TypeScript - Hybrid SSR + TypeScript CRUD approach (keeps existing JSON API) - Event delegation pattern (no inline onclick handlers) - Shared infrastructure: apiClient, toast, event utilities - Procedural/imperative style (no OOP, classes, inheritance) - 6 phases, 16-21 day timeline - Preserves single API for all clients (web, mobile, plugins) - No new backend routes needed
This commit is contained in:
@@ -0,0 +1,992 @@
|
||||
# TypeScript Conversion Plan for Bookhoard
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Convert **~5,500 lines of inline JavaScript** across 13 template files into organized TypeScript modules while **preserving the existing Hybrid SSR architecture**. The web UI will remain a client of the JSON API, alongside Kobo, KOReader, and future plugins.
|
||||
|
||||
**Core Strategy:** Server-Side Rendering (SSR) for initial page loads + TypeScript for CRUD operations via existing JSON API.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### Already Converted to TypeScript ✅
|
||||
| Source | Output | Lines | Purpose |
|
||||
|--------|--------|-------|---------|
|
||||
| `web/src/toast.ts` | `toast.js` | 226 | Toast notifications, HTMX/fetch interceptors |
|
||||
| `web/src/theme.ts` | `theme.js` | 130 | Theme management, smooth scrolling |
|
||||
| `web/src/header.ts` | `header.js` | ~100 | Header dropdowns, theme/user menus |
|
||||
| `web/src/device-management.ts` | `device-management.js` | ~70 | Device token copy/regeneration |
|
||||
|
||||
### Remaining Standalone JavaScript ❌
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `web/static/search.js` | 275 | Header search with keyboard navigation, debouncing |
|
||||
|
||||
### Inline JavaScript in Templates (~5,500 lines)
|
||||
| Template | Script Lines | Primary Functions |
|
||||
|----------|--------------|-------------------|
|
||||
| `collection_rules.templ` | ~400 | Rule CRUD operations, testing |
|
||||
| `unlinked_books.templ` | ~650 | Book matching, linking, bulk operations |
|
||||
| `collections.templ` | ~500 | Bulk operations, filtering |
|
||||
| `devices.templ` | ~350 | Token regeneration, sync URL display |
|
||||
| `bookshelf.templ` | ~250 | Book viewing, pagination |
|
||||
| `dashboard.templ` | ~300 | Statistics, recent activity |
|
||||
| `api_explorer.templ` | ~200 | API testing, cURL generation |
|
||||
| `admin_library.templ` | ~250 | Admin library scan |
|
||||
| `admin_profile.templ` | ~150 | Admin profile management |
|
||||
| `admin.templ` | ~150 | Admin dashboard actions |
|
||||
| `index.templ` | ~100 | Landing page theme preview |
|
||||
| `login.templ` | ~100 | Login theme selection |
|
||||
| `progress.templ` | ~100 | Reading progress |
|
||||
|
||||
**Total: ~5,500 lines of inline JavaScript to convert**
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Hybrid SSR + TypeScript CRUD
|
||||
|
||||
### Current Pattern (Already Working)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User visits /collections │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Go handler: GET /collections │
|
||||
│ 2. Fetches data from CollectionService │
|
||||
│ 3. Renders templates.Collection(user, collections) │
|
||||
│ 4. Returns complete HTML page with data │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
[Page displays instantly with data]
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5. User clicks "Add Rule" button │
|
||||
│ 6. TypeScript: fetch POST /api/collections/{id}/rules │
|
||||
│ 7. Server returns JSON │
|
||||
│ 8. TypeScript updates DOM │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### What This Preserves
|
||||
|
||||
✅ **Fast initial page loads** - SSR with data
|
||||
✅ **Progressive enhancement** - Works without JavaScript
|
||||
✅ **Single API** - All clients use `/api/*` endpoints
|
||||
✅ **Service layer as source of truth** - All handlers use same services
|
||||
✅ **No handler duplication** - No new HTML endpoints needed
|
||||
✅ **Plugin ecosystem ready** - Plugins use JSON API
|
||||
|
||||
---
|
||||
|
||||
## Proposed TypeScript Structure
|
||||
|
||||
```
|
||||
web/ts/
|
||||
├── core/
|
||||
│ ├── toast.ts # ✅ Already converted
|
||||
│ ├── theme.ts # ✅ Already converted
|
||||
│ ├── storage.ts # NEW: localStorage wrapper with types
|
||||
│ ├── dom.ts # NEW: DOM utilities (escapeHtml, querySelector)
|
||||
│ └── api.ts # NEW: API client wrapper with auth
|
||||
│
|
||||
├── features/
|
||||
│ ├── search/
|
||||
│ │ ├── search.ts # Convert from search.js
|
||||
│ │ └── types.ts # Search result types (match Go handlers)
|
||||
│ │
|
||||
│ ├── collections/
|
||||
│ │ ├── rules.ts # Convert from inline JS
|
||||
│ │ ├── bulk.ts # Convert from inline JS
|
||||
│ │ └── types.ts # Reuse handlers.Rule, handlers.Collection
|
||||
│ │
|
||||
│ ├── linking/
|
||||
│ │ ├── matcher.ts # Convert from inline JS
|
||||
│ │ ├── bulk-link.ts # Convert from inline JS
|
||||
│ │ ├── manual-link.ts # Convert from inline JS
|
||||
│ │ └── types.ts # Reuse handlers.BookMatch, handlers.Progress
|
||||
│ │
|
||||
│ ├── devices/
|
||||
│ │ ├── token.ts # Convert from inline JS
|
||||
│ │ └── types.ts # Reuse handlers.Device types
|
||||
│ │
|
||||
│ ├── bookshelf/
|
||||
│ │ ├── display.ts # Convert from inline JS
|
||||
│ │ ├── pagination.ts # Convert from inline JS
|
||||
│ │ └── types.ts # Reuse handlers.MediaItem types
|
||||
│ │
|
||||
│ ├── api-explorer/
|
||||
│ │ ├── request.ts # Convert from inline JS
|
||||
│ │ ├── response.ts # Convert from inline JS
|
||||
│ │ └── curl.ts # Convert from inline JS
|
||||
│ │
|
||||
│ └── admin/
|
||||
│ ├── scan.ts # Convert from inline JS
|
||||
│ └── stats.ts # Convert from inline JS
|
||||
│
|
||||
└── shared/
|
||||
├── events.ts # Event delegation utilities
|
||||
└── auth.ts # Token management helpers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Sharing Strategy
|
||||
|
||||
### Principle: Reuse Go Handler Types
|
||||
|
||||
**❌ DON'T DO THIS:**
|
||||
```typescript
|
||||
// Duplicating types from Go handlers
|
||||
interface Collection {
|
||||
id: string;
|
||||
name: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**✅ DO THIS:**
|
||||
```typescript
|
||||
// Use types that match Go handlers exactly
|
||||
// These types are already defined in Go and returned by /api/*
|
||||
interface MediaItem {
|
||||
id: string;
|
||||
title: string;
|
||||
author?: string;
|
||||
library_id: string;
|
||||
library_type_name: 'ebooks' | 'comics' | 'manga';
|
||||
cover_image_path?: string;
|
||||
}
|
||||
|
||||
// From handlers.CollectionData
|
||||
interface CollectionData {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Description: string;
|
||||
Color: string;
|
||||
Icon: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Single source of truth (Go handlers)
|
||||
- API returns these types as JSON
|
||||
- TypeScript matches exactly what server provides
|
||||
- No duplication, no drift
|
||||
|
||||
---
|
||||
|
||||
## Conversion Phases
|
||||
|
||||
### Phase 1: Complete Standalone File Conversion
|
||||
**Priority: High | Effort: 1 day | Dependencies: None**
|
||||
|
||||
**Tasks:**
|
||||
1. Convert `web/static/search.js` → `web/ts/features/search/search.ts`
|
||||
2. Add proper types for search results (match `handlers.MediaItem`)
|
||||
3. Extract keyboard navigation logic into pure functions
|
||||
4. Update templates to use new TypeScript module
|
||||
|
||||
**Deliverable:** All standalone JavaScript converted to TypeScript
|
||||
|
||||
**Files:**
|
||||
- Create: `web/ts/features/search/search.ts`
|
||||
- Create: `web/ts/features/search/types.ts`
|
||||
- Delete: `web/static/search.js`
|
||||
- Update: Templates referencing search functions
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Core Utilities (Shared Infrastructure)
|
||||
**Priority: High | Effort: 1-2 days | Dependencies: None**
|
||||
|
||||
**Tasks:**
|
||||
1. Create `web/ts/core/storage.ts`
|
||||
- localStorage wrapper with type safety
|
||||
- Token management helpers
|
||||
- Theme persistence
|
||||
|
||||
2. Create `web/ts/core/dom.ts`
|
||||
- escapeHtml utility
|
||||
- querySelector wrappers with null checks
|
||||
- Element creation helpers
|
||||
|
||||
3. Create `web/ts/core/api.ts`
|
||||
- API client wrapper
|
||||
- Automatic auth header injection
|
||||
- Error handling integration with toast system
|
||||
|
||||
4. Create `web/ts/shared/events.ts`
|
||||
- Event delegation helpers
|
||||
- Data attribute selectors
|
||||
- Common event handlers
|
||||
|
||||
**Deliverable:** Reusable utilities for all feature modules
|
||||
|
||||
**Files:**
|
||||
- Create: `web/ts/core/storage.ts`
|
||||
- Create: `web/ts/core/dom.ts`
|
||||
- Create: `web/ts/core/api.ts`
|
||||
- Create: `web/ts/shared/events.ts`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Low Complexity Features (Search, Header, Admin)
|
||||
**Priority: Medium | Effort: 3-4 days | Dependencies: Phase 2**
|
||||
|
||||
**Tasks:**
|
||||
|
||||
**3.1 Search Module (from Phase 1)**
|
||||
- Extract search logic from `search.ts`
|
||||
- Add debounced search with proper types
|
||||
- Keyboard navigation state management
|
||||
- Integration with `/api/media-items/search`
|
||||
|
||||
**3.2 Header Dropdowns**
|
||||
- Extract from `header.ts` (already TS)
|
||||
- Add event delegation for dropdowns
|
||||
- Theme switching logic
|
||||
- User menu interactions
|
||||
|
||||
**3.3 Admin Actions**
|
||||
- Quick scan trigger
|
||||
- System stats display
|
||||
- Admin profile updates
|
||||
|
||||
**Deliverable:** Search, header, and admin features in TypeScript
|
||||
|
||||
**Files:**
|
||||
- Refine: `web/ts/features/search/search.ts`
|
||||
- Update: `web/ts/features/search/types.ts`
|
||||
- Create: `web/ts/features/admin/scan.ts`
|
||||
- Create: `web/ts/features/admin/stats.ts`
|
||||
- Update: `web/src/header.ts` (add event delegation)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Medium Complexity Features (Collections, Bookshelf, Devices)
|
||||
**Priority: Medium | Effort: 4-5 days | Dependencies: Phase 2, 3**
|
||||
|
||||
**Tasks:**
|
||||
|
||||
**4.1 Collection Rules**
|
||||
- Convert rule CRUD from `collection_rules.templ`
|
||||
- Use `/api/collections/{id}/rules` endpoints
|
||||
- Rule testing functionality
|
||||
- Bulk rule operations
|
||||
|
||||
**4.2 Bookshelf Display**
|
||||
- Convert from `bookshelf.templ`
|
||||
- Pagination logic
|
||||
- Library selection state
|
||||
- Book viewing interactions
|
||||
|
||||
**4.3 Device Management**
|
||||
- Convert from `devices.templ`
|
||||
- Token regeneration
|
||||
- Sync URL display
|
||||
- Device registration
|
||||
|
||||
**Deliverable:** Collections, bookshelf, and devices features in TypeScript
|
||||
|
||||
**Files:**
|
||||
- Create: `web/ts/features/collections/rules.ts`
|
||||
- Create: `web/ts/features/collections/bulk.ts`
|
||||
- Create: `web/ts/features/collections/types.ts`
|
||||
- Create: `web/ts/features/bookshelf/display.ts`
|
||||
- Create: `web/ts/features/bookshelf/pagination.ts`
|
||||
- Create: `web/ts/features/devices/token.ts`
|
||||
- Create: `web/ts/features/devices/types.ts`
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: High Complexity Features (Linking, Bulk Operations)
|
||||
**Priority: Low | Effort: 5-6 days | Dependencies: Phase 4**
|
||||
|
||||
**Tasks:**
|
||||
|
||||
**5.1 Book Linking**
|
||||
- Convert matching logic from `unlinked_books.templ`
|
||||
- Search and match functionality
|
||||
- Manual link modal
|
||||
- Bulk auto-link
|
||||
- Bulk get suggestions
|
||||
|
||||
**5.2 API Explorer**
|
||||
- Convert from `api_explorer.templ`
|
||||
- Request/response display
|
||||
- cURL generation
|
||||
- History tracking
|
||||
|
||||
**Deliverable:** Book linking and API explorer in TypeScript
|
||||
|
||||
**Files:**
|
||||
- Create: `web/ts/features/linking/matcher.ts`
|
||||
- Create: `web/ts/features/linking/bulk-link.ts`
|
||||
- Create: `web/ts/features/linking/manual-link.ts`
|
||||
- Create: `web/ts/features/linking/types.ts`
|
||||
- Create: `web/ts/features/api-explorer/request.ts`
|
||||
- Create: `web/ts/features/api-explorer/response.ts`
|
||||
- Create: `web/ts/features/api-explorer/curl.ts`
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Template Integration & Cleanup
|
||||
**Priority: High | Effort: 2-3 days | Dependencies: All phases**
|
||||
|
||||
**Tasks:**
|
||||
|
||||
**6.1 Update Templates**
|
||||
- Replace `<script>` blocks with `<script src="/static/[feature].js">`
|
||||
- Remove inline `onclick="..."` handlers
|
||||
- Add `data-action` attributes for event delegation
|
||||
- Ensure progressive enhancement (pages work without JS)
|
||||
|
||||
**6.2 Verify Progressive Enhancement**
|
||||
- Test all pages with JavaScript disabled
|
||||
- Ensure forms submit with full page reload
|
||||
- Verify critical paths work without JS
|
||||
|
||||
**6.3 Build Configuration**
|
||||
- Update `tsconfig.json` with new file structure
|
||||
- Configure build process for all modules
|
||||
- Add source maps for debugging
|
||||
|
||||
**6.4 Testing**
|
||||
- Verify all features work with TypeScript
|
||||
- Check keyboard navigation
|
||||
- Test form submissions
|
||||
- Validate error handling
|
||||
|
||||
**Deliverable:** Clean templates, all inline JavaScript replaced with TypeScript modules
|
||||
|
||||
---
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### 1. Procedural/Imperative with Functional Techniques
|
||||
|
||||
**✅ GOOD:**
|
||||
```typescript
|
||||
// Pure function, no classes
|
||||
function createRuleItem(rule: Rule): HTMLElement {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'rule-item';
|
||||
div.innerHTML = renderRuleHTML(rule);
|
||||
return div;
|
||||
}
|
||||
|
||||
// Event delegation (one listener)
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const deleteBtn = target.closest('[data-action="delete-rule"]');
|
||||
if (deleteBtn) {
|
||||
const ruleId = deleteBtn.dataset.ruleId;
|
||||
deleteRule(ruleId);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**❌ BAD:**
|
||||
```typescript
|
||||
// Class-based, OOP
|
||||
class RuleManager {
|
||||
private rules: Rule[] = [];
|
||||
constructor() { ... }
|
||||
addRule(rule: Rule) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Type Definitions
|
||||
|
||||
**Reuse Go Handler Types:**
|
||||
```typescript
|
||||
// From internal/handlers/book_matching.go
|
||||
interface UnlinkedBookData {
|
||||
ProgressID: string;
|
||||
DeviceID: string;
|
||||
TitleFromDevice: string;
|
||||
SHA256: string;
|
||||
PotentialMatches: PotentialMatchData[];
|
||||
}
|
||||
|
||||
interface PotentialMatchData {
|
||||
MediaItemID: string;
|
||||
Title: string;
|
||||
Author: string;
|
||||
Confidence: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Only Create Frontend-Specific Types When Necessary:**
|
||||
```typescript
|
||||
// OK: Frontend-specific state
|
||||
type SearchState = {
|
||||
query: string;
|
||||
results: MediaItem[];
|
||||
selectedIndex: number;
|
||||
};
|
||||
|
||||
// OK: UI-specific config
|
||||
type ToastType = 'error' | 'success' | 'info';
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
**Integrate with Toast System:**
|
||||
```typescript
|
||||
async function deleteRule(ruleId: string): Promise<void> {
|
||||
try {
|
||||
const response = await apiClient.delete(`/api/collections/rules/${ruleId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
window.showToast.error(error.message || 'Failed to delete rule');
|
||||
return;
|
||||
}
|
||||
|
||||
window.showToast.success('Rule deleted');
|
||||
removeRuleFromDOM(ruleId);
|
||||
} catch (error) {
|
||||
window.showToast.error('Network error: Unable to connect to server');
|
||||
console.error('Delete rule error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. API Client Pattern
|
||||
|
||||
**Centralized API Client:**
|
||||
```typescript
|
||||
// web/ts/core/api.ts
|
||||
class APIClient {
|
||||
private baseURL = '/api';
|
||||
private getAuthHeader(): string {
|
||||
const token = localStorage.getItem('token');
|
||||
return token ? `Bearer ${token}` : '';
|
||||
}
|
||||
|
||||
async get(url: string): Promise<Response> {
|
||||
return fetch(`${this.baseURL}${url}`, {
|
||||
headers: {
|
||||
'Authorization': this.getAuthHeader(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async post(url: string, data: unknown): Promise<Response> {
|
||||
return fetch(`${this.baseURL}${url}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': this.getAuthHeader(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
async delete(url: string): Promise<Response> {
|
||||
return fetch(`${this.baseURL}${url}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': this.getAuthHeader()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new APIClient();
|
||||
```
|
||||
|
||||
### 5. Event Delegation Pattern
|
||||
|
||||
**Replace Inline Handlers:**
|
||||
```html
|
||||
<!-- ❌ DON'T: Inline onclick -->
|
||||
<button onclick="deleteRule('{rule.id}')">Delete</button>
|
||||
|
||||
<!-- ✅ DO: Data attributes -->
|
||||
<button data-action="delete-rule" data-rule-id="{rule.id}">Delete</button>
|
||||
```
|
||||
|
||||
**Handle with Event Delegation:**
|
||||
```typescript
|
||||
document.addEventListener('click', async (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const deleteBtn = target.closest('[data-action="delete-rule"]');
|
||||
|
||||
if (deleteBtn) {
|
||||
const ruleId = deleteBtn.dataset.ruleId;
|
||||
if (ruleId && confirm('Are you sure you want to delete this rule?')) {
|
||||
await deleteRule(ruleId);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Progressive Enhancement
|
||||
|
||||
**Ensure Works Without JavaScript:**
|
||||
```html
|
||||
<!-- Form submits with full page reload if JS fails -->
|
||||
<form action="/collections/{id}/rules" method="POST">
|
||||
<input type="hidden" name="field" value="title">
|
||||
<input type="hidden" name="operator" value="contains">
|
||||
<input type="text" name="value" required>
|
||||
|
||||
<button type="submit">Add Rule</button>
|
||||
</form>
|
||||
|
||||
<!-- TypeScript enhances with AJAX submit -->
|
||||
<script src="/static/features/collections/rules.js"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build & Deployment
|
||||
|
||||
### TypeScript Configuration
|
||||
|
||||
**Update `tsconfig.json`:**
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "none",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./web/static",
|
||||
"rootDir": "./web/ts",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"web/ts/**/*.ts",
|
||||
"web/src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Build Scripts
|
||||
|
||||
**Update `package.json`:**
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch",
|
||||
"build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify",
|
||||
"build:ts": "tsc",
|
||||
"build:ts:watch": "tsc --watch",
|
||||
"build:ts:prod": "tsc --sourceMap false",
|
||||
"build:all": "npm run build:ts && npm run build:css:prod",
|
||||
"dev": "npm run build:ts:watch & npm run build:css"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
**Compiled JavaScript:**
|
||||
```
|
||||
web/static/
|
||||
├── core/
|
||||
│ ├── toast.js # ✅ Already exists
|
||||
│ ├── theme.js # ✅ Already exists
|
||||
│ ├── header.js # ✅ Already exists
|
||||
│ ├── device-management.js # ✅ Already exists
|
||||
│ ├── storage.js # NEW
|
||||
│ ├── dom.js # NEW
|
||||
│ └── api.js # NEW
|
||||
├── features/
|
||||
│ ├── search/
|
||||
│ │ └── search.js # NEW
|
||||
│ ├── collections/
|
||||
│ │ ├── rules.js # NEW
|
||||
│ │ └── bulk.js # NEW
|
||||
│ ├── linking/
|
||||
│ │ ├── matcher.js # NEW
|
||||
│ │ ├── bulk-link.js # NEW
|
||||
│ │ └── manual-link.js # NEW
|
||||
│ ├── bookshelf/
|
||||
│ │ ├── display.js # NEW
|
||||
│ │ └── pagination.js # NEW
|
||||
│ ├── devices/
|
||||
│ │ └── token.js # NEW
|
||||
│ ├── api-explorer/
|
||||
│ │ ├── request.js # NEW
|
||||
│ │ ├── response.js # NEW
|
||||
│ │ └── curl.js # NEW
|
||||
│ └── admin/
|
||||
│ ├── scan.js # NEW
|
||||
│ └── stats.js # NEW
|
||||
└── shared/
|
||||
├── events.js # NEW
|
||||
└── auth.js # NEW
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 1. Unit Testing (Optional)
|
||||
Test pure functions that don't depend on DOM:
|
||||
```typescript
|
||||
// Test utilities
|
||||
describe('escapeHtml', () => {
|
||||
it('should escape HTML entities', () => {
|
||||
expect(escapeHtml('<script>')).to.equal('<script>');
|
||||
});
|
||||
});
|
||||
|
||||
// Test formatters
|
||||
describe('formatConfidence', () => {
|
||||
it('should format confidence as percentage', () => {
|
||||
expect(formatConfidence(0.95)).to.equal('95%');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Integration Testing
|
||||
Test API interactions:
|
||||
```typescript
|
||||
describe('deleteRule', () => {
|
||||
it('should call DELETE /api/collections/rules/{id}', async () => {
|
||||
const fetchSpy = sinon.stub(global, 'fetch');
|
||||
await deleteRule('rule-123');
|
||||
|
||||
expect(fetchSpy.calledWith('/api/collections/rules/rule-123', {
|
||||
method: 'DELETE'
|
||||
})).to.be.true;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Manual Testing Checklist
|
||||
- [ ] All CRUD operations work (create, read, update, delete)
|
||||
- [ ] Toast notifications display correctly
|
||||
- [ ] Error handling works (network errors, 500 errors, 404 errors)
|
||||
- [ ] Keyboard navigation works (search, modals)
|
||||
- [ ] Forms submit correctly
|
||||
- [ ] Bulk operations work
|
||||
- [ ] Progressive enhancement (test with JS disabled)
|
||||
- [ ] All pages load with SSR data
|
||||
- [ ] No console errors
|
||||
|
||||
### 4. Browser Testing
|
||||
Test in:
|
||||
- Chrome/Edge (Chromium)
|
||||
- Firefox
|
||||
- Safari (if available)
|
||||
- Mobile browsers (responsive testing)
|
||||
|
||||
---
|
||||
|
||||
## Progressive Enhancement Verification
|
||||
|
||||
### Test Without JavaScript
|
||||
|
||||
**Disable JavaScript in browser:**
|
||||
1. Open DevTools
|
||||
2. Settings → Disable JavaScript
|
||||
3. Reload page
|
||||
4. Verify critical functionality works
|
||||
|
||||
**What Should Work:**
|
||||
- ✅ Page loads with SSR data
|
||||
- ✅ Navigation links work (full page reload)
|
||||
- ✅ Forms submit (full page reload, not AJAX)
|
||||
- ✅ All data visible on initial load
|
||||
|
||||
**What Won't Work (Expected):**
|
||||
- ❌ Dynamic updates without page reload
|
||||
- ❌ Toast notifications
|
||||
- ❌ Keyboard navigation
|
||||
- ❌ Modal dialogs
|
||||
- ❌ Auto-complete search
|
||||
|
||||
**Mitigation:**
|
||||
- Ensure forms have proper `action` and `method` attributes
|
||||
- Provide submit buttons for all operations
|
||||
- Include helpful error messages in HTML responses
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Potential Issues
|
||||
|
||||
**1. Breaking Changes During Conversion**
|
||||
- **Risk**: Converting inline JS breaks functionality
|
||||
- **Mitigation**: Convert incrementally, test each feature
|
||||
- **Rollback**: Keep inline JS until TypeScript is verified
|
||||
|
||||
**2. Type Mismatches with Go Handlers**
|
||||
- **Risk**: TypeScript types don't match JSON responses
|
||||
- **Mitigation**: Use Go handler types as source of truth
|
||||
- **Validation**: Test API responses match TypeScript types
|
||||
|
||||
**3. Progressive Enhancement Regression**
|
||||
- **Risk**: Pages require JavaScript to function
|
||||
- **Mitigation**: Test with JS disabled before/after
|
||||
- **Validation**: Forms must have action/method attributes
|
||||
|
||||
**4. Build Complexity**
|
||||
- **Risk**: TypeScript compilation becomes bottleneck
|
||||
- **Mitigation**: Use existing `tsc` setup, add watch mode
|
||||
- **Validation**: Build time under 5 seconds
|
||||
|
||||
**5. Bundle Size**
|
||||
- **Risk**: Too much JavaScript loaded
|
||||
- **Mitigation**: Split by feature, load per-page
|
||||
- **Validation**: Monitor bundle sizes, code splitting
|
||||
|
||||
### Rollback Strategy
|
||||
|
||||
**If conversion fails:**
|
||||
1. Git revert TypeScript files
|
||||
2. Inline JavaScript still works (wasn't deleted yet)
|
||||
3. Fix issues and retry
|
||||
4. Delete inline JS only after TypeScript verified
|
||||
|
||||
**Per-feature rollback:**
|
||||
```bash
|
||||
# Before converting feature
|
||||
git branch backup-before-collections
|
||||
|
||||
# If collections conversion breaks
|
||||
git checkout backup-before-collections -- templates/collections.templ
|
||||
|
||||
# Fix TypeScript and try again
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
|
||||
| Phase | Duration | Dependencies | Deliverables |
|
||||
|-------|----------|--------------|--------------|
|
||||
| Phase 1: Standalone Conversion | 1 day | None | Search converted to TS |
|
||||
| Phase 2: Core Utilities | 1-2 days | None | Storage, DOM, API, events modules |
|
||||
| Phase 3: Low Complexity | 3-4 days | Phase 2 | Search, header, admin in TS |
|
||||
| Phase 4: Medium Complexity | 4-5 days | Phase 2, 3 | Collections, bookshelf, devices in TS |
|
||||
| Phase 5: High Complexity | 5-6 days | Phase 4 | Linking, API explorer in TS |
|
||||
| Phase 6: Integration & Cleanup | 2-3 days | All phases | Clean templates, verified functionality |
|
||||
| **Total** | **16-21 days** | | **Full TypeScript conversion** |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All inline JavaScript removed from templates
|
||||
- [ ] All features work with TypeScript
|
||||
- [ ] TypeScript compilation passes with strict mode
|
||||
- [ ] Progressive enhancement maintained (pages work without JS)
|
||||
- [ ] No type `any` (except for legacy API responses)
|
||||
- [ ] Event delegation replaces inline handlers
|
||||
- [ ] Build process integrated into npm scripts
|
||||
- [ ] Full manual testing completed
|
||||
- [ ] No regression in functionality
|
||||
- [ ] Bundle sizes monitored and optimized
|
||||
- [ ] SSR still works for all initial page loads
|
||||
- [ ] All CRUD operations use existing `/api/*` endpoints
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review and approve** this plan
|
||||
2. **Setup Phase 1**: Convert `search.js` → TypeScript
|
||||
3. **Begin Phase 2**: Create core utility modules
|
||||
4. **Iterate** through phases 3-5 (feature conversion)
|
||||
5. **Phase 6**: Final integration and cleanup
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Template Integration Examples
|
||||
|
||||
### Before: Inline JavaScript
|
||||
|
||||
```html
|
||||
<!-- templates/collection_rules.templ -->
|
||||
<script>
|
||||
let collectionId = '{ collection.ID }';
|
||||
|
||||
function renderRules(rules) {
|
||||
const container = document.getElementById('rules-container');
|
||||
container.innerHTML = rules.map((rule, index) => `
|
||||
<div class="rule-item">
|
||||
<span>${rule.field}</span>
|
||||
<button onclick="deleteRule('${rule.id}')">Delete</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function deleteRule(ruleId) {
|
||||
fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
loadRules();
|
||||
showToast('Rule deleted');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadRules() {
|
||||
fetch(`/api/collections/${collectionId}/rules`)
|
||||
.then(response => response.json())
|
||||
.then(rules => renderRules(rules));
|
||||
}
|
||||
|
||||
loadRules();
|
||||
</script>
|
||||
```
|
||||
|
||||
### After: TypeScript Module + Event Delegation
|
||||
|
||||
**TypeScript Module:**
|
||||
```typescript
|
||||
// web/ts/features/collections/rules.ts
|
||||
import { apiClient } from '../../core/api.js';
|
||||
import { escapeHtml } from '../../core/dom.js';
|
||||
|
||||
interface Rule {
|
||||
id: string;
|
||||
field: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function renderRules(rules: Rule[]): void {
|
||||
const container = document.getElementById('rules-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = rules.map(rule => `
|
||||
<div class="rule-item" data-rule-id="${rule.id}">
|
||||
<span>${escapeHtml(rule.field)}</span>
|
||||
<button data-action="delete-rule" data-rule-id="${rule.id}">Delete</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadRules(collectionId: string): Promise<void> {
|
||||
try {
|
||||
const response = await apiClient.get(`/collections/${collectionId}/rules`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load rules');
|
||||
}
|
||||
|
||||
const rules: Rule[] = await response.json();
|
||||
renderRules(rules);
|
||||
} catch (error) {
|
||||
console.error('Load rules error:', error);
|
||||
window.showToast.error('Failed to load rules');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(collectionId: string, ruleId: string): Promise<void> {
|
||||
if (!confirm('Are you sure you want to delete this rule?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.delete(`/collections/${collectionId}/rules/${ruleId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete rule');
|
||||
}
|
||||
|
||||
window.showToast.success('Rule deleted');
|
||||
|
||||
// Remove from DOM
|
||||
const ruleElement = document.querySelector(`[data-rule-id="${ruleId}"]`);
|
||||
ruleElement?.remove();
|
||||
} catch (error) {
|
||||
console.error('Delete rule error:', error);
|
||||
window.showToast.error('Failed to delete rule');
|
||||
}
|
||||
}
|
||||
|
||||
// Event delegation for delete buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const deleteBtn = target.closest('[data-action="delete-rule"]');
|
||||
|
||||
if (deleteBtn) {
|
||||
const ruleId = deleteBtn.dataset.ruleId;
|
||||
const collectionId = deleteBtn.dataset.collectionId;
|
||||
|
||||
if (ruleId && collectionId) {
|
||||
deleteRule(collectionId, ruleId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const collectionId = document.body.dataset.collectionId;
|
||||
if (collectionId) {
|
||||
loadRules(collectionId);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Cleaned Template:**
|
||||
```html
|
||||
<!-- templates/collection_rules.templ -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/core/toast.js"></script>
|
||||
<script src="/static/core/api.js"></script>
|
||||
<script src="/static/features/collections/rules.js"></script>
|
||||
</head>
|
||||
<body data-collection-id="{ collection.ID }">
|
||||
<div id="rules-container">
|
||||
<!-- SSR: Initial rules rendered server-side -->
|
||||
{ range .Rules }
|
||||
<div class="rule-item" data-rule-id="{ .ID }">
|
||||
<span>{ .Field }</span>
|
||||
<button data-action="delete-rule"
|
||||
data-rule-id="{ .ID }"
|
||||
data-collection-id="{ collection.ID }">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
{ end }
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ No inline JavaScript
|
||||
- ✅ Event delegation (one listener)
|
||||
- ✅ Type-safe API calls
|
||||
- ✅ Better error handling
|
||||
- ✅ Reusable module
|
||||
- ✅ SSR still works (initial rules in HTML)
|
||||
- ✅ Progressive enhancement (form can submit without JS)
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2025-02-17*
|
||||
*Follows: PROJECT_GUIDELINES.md*
|
||||
*Architecture: Hybrid SSR + TypeScript CRUD*
|
||||
Reference in New Issue
Block a user