- Deleted templates.CollectionDetailData - using templates.CollectionData everywhere - Deleted templates.BookData - using handlers.BookInfo everywhere - Deleted templates.DeviceData - using handlers.DeviceInfo everywhere - Deleted templates.ProgressItemData - using handlers.ProgressWithMedia everywhere - Deleted templates.convertDevices() helper - Use handlers types directly in templates - Enhanced handlers.ProgressWithMedia with device metadata fields - Added handlers.getDeviceIcon() helper - Updated all templates to import handlers package - Cleaned up unused imports This aligns codebase with templ's design philosophy (use Go types directly, no parallel type system)
475 lines
14 KiB
Markdown
475 lines
14 KiB
Markdown
# Type Duplication Refactoring Plan
|
|
|
|
**Document Purpose**: Consolidate duplicate type definitions across templates/ and handlers/ packages to align with templ's design philosophy and reduce maintenance burden.
|
|
|
|
**Status**: Planning Phase
|
|
**Last Updated**: 2026-02-12
|
|
**Separation from**: IMPLEMENTATION_EXACT.md (this is independent refactoring)
|
|
|
|
**Core Principle**: **API wins** - When there's ambiguity between template types and API/handler types, the API/handler types take precedence.
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
**Problem**: Codebase maintains parallel type systems:
|
|
- `templates/types.go` - Template-specific types
|
|
- `handlers/*.go` - API response types
|
|
|
|
**Core Principle**: **API wins - when in doubt, API/handler types take precedence**
|
|
|
|
### Why "API wins"?
|
|
|
|
1. **API is public contract**: Handler types define your public API surface - these are more stable
|
|
2. **Templates are presentation**: HTML templates can format any Go type - they're flexible
|
|
3. **Single source of truth**: One authoritative type definition vs two parallel definitions
|
|
4. **Json serialization**: Handler types already have `json:"..."` tags for API responses
|
|
5. **Future API clients**: If you add mobile apps or CLI tools, they need handler types anyway
|
|
|
|
### Trade-offs
|
|
|
|
**Pro**: Simpler codebase, one type to maintain, automatic API/HTML consistency
|
|
**Con**: Templates may have unused fields visible in scope (acceptable - templ handles this well)
|
|
|
|
**Impact**:
|
|
- ~40 lines of duplicate type definitions
|
|
- Unnecessary conversion functions
|
|
- Maintenance burden (update 2 places for 1 change)
|
|
- Violates templ's design philosophy
|
|
|
|
**Solution**: Use handler types directly in templates, eliminate template-specific duplicates.
|
|
|
|
---
|
|
|
|
## Current State Analysis
|
|
|
|
### Duplicates Found
|
|
|
|
| Template Type | Handler Type | Overlap | Action |
|
|
|---------------|---------------|----------|--------|
|
|
| `DeviceData` | `DeviceInfo` | 90% | **DELETE** DeviceData |
|
|
| `BookData` | `collections.BookInfo` | 100% | **DELETE** BookData |
|
|
| `CollectionDetailData` | `CollectionData` | 100% | **DELETE** CollectionDetailData |
|
|
| `ProgressItemData` | `ProgressWithMedia` | 70% | **MERGE** (different fields) |
|
|
| `QueueItemData` | `QueueItemResponse` | 60% | **KEEP** (template uses subset) |
|
|
|
|
### Already Using Handler Types (Good Patterns)
|
|
|
|
✅ `Conflicts` template uses `handlers.ConflictDetailResponse`
|
|
✅ `Queue` template uses `handlers.QueueItemResponse`
|
|
|
|
**This is the pattern we want everywhere.**
|
|
|
|
---
|
|
|
|
## Refactoring Plan
|
|
|
|
### Phase 1: Eliminate Complete Duplicates
|
|
|
|
#### 1.1 Delete CollectionDetailData
|
|
|
|
**Files to Modify**:
|
|
- `templates/types.go` (lines 26-32) - DELETE struct
|
|
- `templates/collection_rules.templ` (line 3) - Change signature
|
|
- `templates/collections.templ` (line 213) - Change signature
|
|
|
|
**Changes**:
|
|
|
|
**Delete** from `templates/types.go`:
|
|
```go
|
|
type CollectionDetailData struct {
|
|
ID string
|
|
Name string
|
|
Description string
|
|
Color string
|
|
Icon string
|
|
}
|
|
```
|
|
|
|
**Update** `templates/collection_rules.templ:3`:
|
|
```templ
|
|
// BEFORE:
|
|
templ CollectionRules(user User, collection CollectionDetailData) {
|
|
|
|
// AFTER:
|
|
templ CollectionRules(user User, collection templates.CollectionData) {
|
|
```
|
|
|
|
**Update** `templates/collections.templ:213`:
|
|
```templ
|
|
// BEFORE:
|
|
templ CollectionDetail(user User, collection CollectionDetailData, books []BookData) {
|
|
|
|
// AFTER:
|
|
templ CollectionDetail(user User, collection templates.CollectionData, books []collections.BookInfo) {
|
|
```
|
|
|
|
**Test**: `go test ./templates/...` after template regeneration
|
|
|
|
---
|
|
|
|
#### 1.2 Delete BookData
|
|
|
|
**Files to Modify**:
|
|
- `templates/types.go` (lines 34-39) - DELETE struct
|
|
- `templates/collections.templ` (line 213) - Change books param
|
|
|
|
**Changes**:
|
|
|
|
**Delete** from `templates/types.go`:
|
|
```go
|
|
type BookData struct {
|
|
MediaItemID string
|
|
Title string
|
|
Author string
|
|
CoverImagePath string
|
|
}
|
|
```
|
|
|
|
**Update** `templates/collections.templ:213`:
|
|
```templ
|
|
// BEFORE:
|
|
templ CollectionDetail(user User, collection templates.CollectionData, books []BookData) {
|
|
|
|
// AFTER:
|
|
templ CollectionDetail(user User, collection templates.CollectionData, books []collections.BookInfo) {
|
|
```
|
|
|
|
**Test**: View collection detail page, verify books display correctly
|
|
|
|
---
|
|
|
|
#### 1.3 Delete DeviceData and ConvertDevices
|
|
|
|
**Files to Modify**:
|
|
- `templates/types.go` (lines 48-57) - DELETE struct
|
|
- `templates/devices.templ` (line 3) - Change signature
|
|
- `internal/router/helpers.go` (lines 46-67) - DELETE function
|
|
- `internal/router/frontend.go` (lines 161-169) - Remove conversion
|
|
|
|
**Changes**:
|
|
|
|
**Delete** from `templates/types.go`:
|
|
```go
|
|
type DeviceData struct {
|
|
ID string
|
|
DeviceName string
|
|
DeviceType string
|
|
SyncEnabled bool
|
|
AutoSync bool
|
|
SyncFrequency int32
|
|
LastSync string
|
|
LastSeen string
|
|
}
|
|
```
|
|
|
|
**Delete** from `internal/router/helpers.go`:
|
|
```go
|
|
func convertDevices(deviceInfos []handlers.DeviceInfo) []templates.DeviceData {
|
|
// ... entire function (lines 46-67)
|
|
}
|
|
```
|
|
|
|
**Update** `templates/devices.templ:3`:
|
|
```templ
|
|
// BEFORE:
|
|
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
|
|
|
|
// AFTER:
|
|
templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) {
|
|
```
|
|
|
|
**Update** `templates/devices.templ` template logic:
|
|
|
|
Find these patterns and update:
|
|
```templ
|
|
// BEFORE:
|
|
if device.LastSync != "" {
|
|
<span>{ device.LastSync }</span>
|
|
}
|
|
|
|
// AFTER:
|
|
if device.LastSync != nil {
|
|
<span>{ device.LastSync.Format("2006-01-02 15:04") }</span>
|
|
}
|
|
```
|
|
|
|
**Update** `internal/router/frontend.go:161-169`:
|
|
```go
|
|
// BEFORE:
|
|
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
|
}
|
|
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
|
}
|
|
deviesList := convertDevices(deviceData)
|
|
pendingList := convertPending(pendingData)
|
|
|
|
// AFTER:
|
|
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
|
}
|
|
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
|
if err != nil {
|
|
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
|
}
|
|
// No conversion - use handler data directly
|
|
```
|
|
|
|
**Test**: Devices page renders correctly with proper date formatting
|
|
|
|
---
|
|
|
|
### Phase 2: Eliminate Remaining Template Types
|
|
|
|
#### 2.1 ProgressItemData → Use ProgressWithMedia
|
|
|
|
**Current Issue**:
|
|
- `handlers.ProgressWithMedia` exists as API response type
|
|
- `templates.ProgressItemData` duplicates it with minor variations
|
|
|
|
**Decision**: **API wins** - Use `handlers.ProgressWithMedia` everywhere
|
|
|
|
**Files to Modify**:
|
|
- `internal/handlers/progress.go` - Update `ProgressWithMedia` struct
|
|
- `templates/types.go` - DELETE `ProgressItemData`
|
|
- `templates/progress.templ` - Use `handlers.ProgressWithMedia`
|
|
|
|
**Changes**:
|
|
|
|
**Add fields to** `internal/handlers/progress.go:ProgressWithMedia`:
|
|
```go
|
|
// ADD fields to existing struct:
|
|
type ProgressWithMedia struct {
|
|
MediaItemID uuid.UUID `json:"media_item_id"`
|
|
Title string `json:"title"`
|
|
Author string `json:"author"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
Percentage float64 `json:"percentage"`
|
|
CurrentPage int32 `json:"current_page"`
|
|
TotalPages int32 `json:"total_pages"`
|
|
LastReadAt time.Time `json:"last_read_at"`
|
|
Epubcfi string `json:"epubcfi"`
|
|
LastSyncDevice string `json:"last_sync_device"`
|
|
DeviceName string `json:"device_name,omitempty"` // ADD
|
|
DeviceType string `json:"device_type,omitempty"` // ADD
|
|
DeviceIcon string `json:"device_icon,omitempty"` // ADD
|
|
}
|
|
```
|
|
|
|
**Update** `internal/handlers/progress.go:GetAllProgress()` to populate new device fields.
|
|
|
|
**Delete** from `templates/types.go` (lines 99-112):
|
|
```go
|
|
type ProgressItemData struct {
|
|
// ... delete entire struct
|
|
}
|
|
```
|
|
|
|
**Update** `templates/progress.templ:3`:
|
|
```templ
|
|
// BEFORE:
|
|
templ Progress(user User, progressData []ProgressItemData) {
|
|
|
|
// AFTER:
|
|
templ Progress(user User, progressData []handlers.ProgressWithMedia) {
|
|
```
|
|
|
|
**Test**: Progress page displays device names and icons
|
|
|
|
**Delete** from `templates/types.go` (lines 99-112):
|
|
```go
|
|
type ProgressItemData struct {
|
|
// ... delete entire struct
|
|
}
|
|
```
|
|
|
|
**Update** `templates/progress.templ:3`:
|
|
```templ
|
|
// BEFORE:
|
|
templ Progress(user User, progressData []ProgressItemData) {
|
|
|
|
// AFTER:
|
|
templ Progress(user User, progressData []handlers.ProgressWithMedia) {
|
|
```
|
|
|
|
**Test**: Progress page displays device names and icons
|
|
|
|
---
|
|
|
|
### Phase 3: Update Get*Data Functions
|
|
|
|
**Issue**: Several handler functions return handler types but aren't used directly by templates due to conversion layer.
|
|
|
|
**Files to Modify**:
|
|
- `internal/router/frontend.go` - Remove conversions
|
|
|
|
**Changes**:
|
|
|
|
**Devices** (already done in Phase 1.3):
|
|
- `GetDevicesData()` returns `[]handlers.DeviceInfo`
|
|
- Template uses directly
|
|
|
|
**Progress**:
|
|
```go
|
|
// frontend.go - update progress route
|
|
protected.GET("/progress-page", func(c echo.Context) error {
|
|
// ...
|
|
progressData, err := cfg.Handler.GetAllProgressData(c)
|
|
// Returns []handlers.ProgressWithMedia directly - no conversion
|
|
// ...
|
|
})
|
|
```
|
|
|
|
**Collections**:
|
|
```go
|
|
// collections.go - update to return handler types
|
|
func (h *CollectionHandler) GetCollectionBooksData(c echo.Context) ([]collections.BookInfo, error) {
|
|
// Already returns BookInfo - just update template
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary Table
|
|
|
|
| Phase | Files Changed | Lines Removed | Lines Added | Net Change |
|
|
|-------|--------------|---------------|--------------|-------------|
|
|
| 1.1 CollectionDetailData | 3 | 7 | 2 | -5 |
|
|
| 1.2 BookData | 3 | 6 | 0 | -6 |
|
|
| 1.3 DeviceData | 4 | 24 | 5 | -19 |
|
|
| 2.1 ProgressItemData | 4 | 14 | 3 | -11 |
|
|
| **Total** | **14** | **51** | **10** | **-41** |
|
|
|
|
**Note**: QueueItemData already uses handler type (good pattern). Conflicts already uses handler type (good pattern).
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
After each phase:
|
|
|
|
### Phase 1.1 (CollectionDetailData)
|
|
- [ ] View collection rules page
|
|
- [ ] Edit collection rules
|
|
- [ ] View collection detail page
|
|
- [ ] Add books to collection
|
|
|
|
### Phase 1.2 (BookData)
|
|
- [ ] View collection detail page
|
|
- [ ] Browse books in collection
|
|
- [ ] Book covers display correctly
|
|
|
|
### Phase 1.3 (DeviceData)
|
|
- [ ] Devices page loads
|
|
- [ ] Device cards display
|
|
- [ ] Last sync/last seen dates formatted
|
|
- [ ] Device type icons show correctly
|
|
|
|
### Phase 2.1 (ProgressItemData)
|
|
- [ ] Progress page loads
|
|
- [ ] Progress entries show device names/icons
|
|
- [ ] Percentage display works
|
|
- [ ] Cover images display
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
If refactoring breaks functionality:
|
|
|
|
1. **Git stash** changes before starting
|
|
2. **Commit after each successful phase**
|
|
3. **Revert individual commits** if needed
|
|
4. **Keep conversion functions** commented out temporarily
|
|
|
|
```bash
|
|
# Before starting
|
|
git stash push -m "pre-refactor-state"
|
|
|
|
# After Phase 1.1
|
|
git commit -m "refactor: delete CollectionDetailData, use CollectionData"
|
|
|
|
# If Phase 1.2 breaks
|
|
git revert HEAD
|
|
# Continue with 1.3
|
|
```
|
|
|
|
---
|
|
|
|
## Benefits
|
|
|
|
### Code Quality
|
|
- ✅ Aligns with templ's design philosophy
|
|
- ✅ Single source of truth for types
|
|
- ✅ Reduced code duplication
|
|
- ✅ **API-first**: Handler types are authoritative
|
|
|
|
### Maintenance
|
|
- ✅ Update types in 1 place, not 2
|
|
- ✅ Less conversion logic to maintain
|
|
- ✅ Clearer data flow
|
|
- ✅ **Template changes automatically get API updates**
|
|
|
|
### Consistency
|
|
- ✅ All templates use handlers types (like Queue/Conflicts already do)
|
|
- ✅ API and HTML use same type definitions
|
|
- ✅ 41 fewer lines of code
|
|
- ✅ **"API wins"** principle eliminates ambiguity
|
|
|
|
---
|
|
|
|
## Future Considerations
|
|
|
|
### For IMPLEMENTATION_EXACT.md
|
|
After this refactoring, adding `AuthToken` to devices is simpler:
|
|
- Add `AuthToken string` to `handlers.DeviceInfo` struct
|
|
- Add to `GetDevicesData()` function return values
|
|
- Template automatically gets it (no conversion layer)
|
|
|
|
This **solves the critical issue** found in IMPLEMENTATION_EXACT.md analysis automatically.
|
|
|
|
### For New Features
|
|
- Define handler types with json tags first
|
|
- Templates use same types directly
|
|
- Only create template-specific types for:
|
|
- Computed/derived fields not in API
|
|
- Combining data from multiple sources
|
|
- Simplifying complex nested structures
|
|
|
|
---
|
|
|
|
## Execution Order
|
|
|
|
1. **Run tests** to ensure baseline: `go test ./...`
|
|
2. **Create feature branch**: `git checkout -b refactor/types-consolidation`
|
|
3. **Execute Phase 1.1** (CollectionDetailData) - DELETE template type, use API type
|
|
4. **Commit and test**
|
|
5. **Execute Phase 1.2** (BookData) - DELETE template type, use API type
|
|
6. **Commit and test**
|
|
7. **Execute Phase 1.3** (DeviceData) - DELETE template type, use API type
|
|
8. **Commit and test**
|
|
9. **Execute Phase 2.1** (ProgressItemData) - DELETE template type, enhance API type
|
|
10. **Final commit and test**
|
|
11. **Update IMPLEMENTATION_EXACT.md** to reference handler types only
|
|
|
|
**Total Estimated Time**: 2-3 hours with testing
|
|
|
|
**Key Mindset**: Every change removes a template type, never the reverse. API types are authoritative.
|
|
|
|
---
|
|
|
|
## Questions for User
|
|
|
|
1. **Progress template updates**: Since we're using `handlers.ProgressWithMedia` (API type) in templates, any template formatting changes (like date formatting) should be done in the template itself. Is this acceptable?
|
|
|
|
2. **Testing**: Do you have automated template tests, or is this manual testing only?
|
|
|
|
3. **Implementation plan**: Should I update IMPLEMENTATION_EXACT.md after this refactoring to reference `handlers.DeviceInfo` instead of `DeviceData`?
|
|
|
|
4. **Json tags on handler types**: Templates don't need `json:"..."` tags - should we document that these are for API responses only?
|