Files
bookhoard/templates/types.go
T
John O'Keefe 0a7fbd6f48 feat(collections): smart (dynamic) collections — rules evaluated live at render
Type is DERIVED, not stored: a saved ruleset makes a collection SMART,
no rules = STATIC (stored collection_items links, unchanged behavior).
No schema change — collections.auto_assign_rules JSONB already carried
the config; user's decision 2026-10-14 (handoff: live-at-render, not
refresh-on-scan; create-time Static/Smart toggle; v1 NO hybrid).

- Rule gains an enabled flag (absent = enabled, legacy rules unaffected).
- Rule CRUD: POST/PUT(DELETE) /api/collections/:id/rules[/:ruleId] — the
  survivor dead effort of the rules-builder UI finally has real
  endpoints (rules were never editable after creation; nothing ever
  re-evaluated them). First rule converts a user collection to smart;
  removing the last reverts it to static. UpdateCollectionRulesJSON
  edits ONLY the rules doc so rule CRUD cannot clobber metadata.
- Live evaluation: candidates from GetVisibleLiveMediaForRules —
  visible-library set (family lens enforced at source) + archived IS
  NULL AND missing_scan_count = 0 — any enabled rule matches, title
  order. Zero refresh hooks by design: always current by construction.
- Membership mutations (add/remove/bulk-add) are 400 on smart — v1 no
  hybrid; collection_items.excluded stays the reserved future hook.
- Rules builder page (/collections/:id/rules) first time ever routable;
  templ whole-attribute interpolation ("{ expr }" inside a quoted attr
  value is literal text, as theme-{ user.Theme } silently proved).
- UI: create modal Static/⚡Smart cards (smart create redirects into the
  rules builder), ⚡ badge + real per-card counts on the grid (smart =
  live matches, static = stored-link count), smart detail renders
  matched books with no hand-pick actions + an Edit Rules button.
- API: GET /:id gains is_smart + live books; GET /: keeps the Android
  app's default exclusion, include_auto=true surfaces smart rows with
  live counts; TestRules previews against the SAME candidates the live
  render uses (visible + live, was raw first-1000).
- FIXED pre-existing regression from 7ce7321: static collection detail
  with NO library_id returned zero books — QueryType != '' caught user
  rows ('filter' default) and routed them to dashboard sections that
  never contain them; now only real system query types go there
  (handlers + SSR). App-visible symptom: /api/collections/:id without
  library_id came back books:[] for every static collection.

Tests: collections_smart_test.go (conversion, disabled rules, mutation
guards, revert-to-static, validation, system-collection prohibition,
include_auto semantics). Full offline integration suite PASS.
2026-10-14 11:59:38 -04:00

310 lines
6.7 KiB
Go

package templates
import (
"time"
"github.com/a-h/templ"
)
type User struct {
ID string
Username string
Email string
Role string
Theme string
FirstName string
LastName string
CreatedAt time.Time
Token string
Timezone string
}
type PageData struct {
User User
CurrentPage string
}
type CollectionData struct {
ID string
Name string
Description string
Color string
Icon string
IsSystem bool
// IsSmart marks rule-based ("dynamic") collections: membership is
// evaluated live at render, not stored in collection_items.
IsSmart bool
// BookCount is the LIVE count for the card (rule matches for smart
// collections, stored links for static ones).
BookCount int
}
type LibraryData struct {
ID string
Name string
Description string
TypeName string
TypeValue string
MediaCount int64
FolderCount int
// Offline is true when none of the library's folders exist on disk
// (e.g. an unmounted external drive). Contents stay intact and reappear
// when the storage returns.
Offline bool
}
type FolderData struct {
FolderPath string
}
// ArchivedItem feeds the admin archived-items page: media hidden from
// libraries because their files vanished, with their pending fate. When an
// active item with identical content (same SHA-256) exists, MatchID and
// friends are set so the UI can offer merging the archived row's reading
// data into its active twin.
type ArchivedItem struct {
ID string
Title string
Author string
LibraryName string
FilePath string
MissingScans int32
ArchivedAt *time.Time
PurgeAt *time.Time
Status string
MatchID string
MatchTitle string
MatchLibraryName string
}
type DirEntry struct {
Name string
Path string
}
type UserVisibilityData struct {
UserID string
Username string
Email string
IsVisible bool
}
type AdminStats struct {
LibraryCount int
MediaCount int
UserCount int
DeviceCount int
}
type ScanSettingsData struct {
AutoScanEnabled bool
ScanPollIntervalSeconds int
}
// SettingEntry mirrors database.SettingEntry for the admin UI. Kept as a
// template-local type so the templates package does not import database.
type SettingEntry struct {
Key string
Value string
Type string
Min string
Max string
RequiresRestart bool
Category string
Group string
Description string
IsDefault bool
}
// SettingGroup is a labeled cluster of related settings rendered as a
// sub-section within a tunable-settings card.
type SettingGroup struct {
Name string
Entries []SettingEntry
}
type SeriesCardData struct {
Name string
LibraryID string
BookCount int64
TotalInSeries int
CoverPaths []string
}
type PendingRegistrationData struct {
RegistrationID string
DeviceName string
DeviceType string
ExpiresAt string
}
type ConflictData struct {
ID string
MediaItemID string
MediaItemTitle string
ConflictType string
ResolutionStatus string
CreatedAt string
ResolutionData map[string]interface{}
ResolvedBy string
ResolvedAt string
}
type QueueItemData struct {
ID string
DeviceID string
DeviceName string
DeviceType string
SyncType string
Status string
Priority int32
Attempts int32
CreatedAt string
}
type DeviceShelfMappingData struct {
ID string
CollectionID string
CollectionName string
DeviceShelfName string
SyncDirection string
CreatedAt string
}
type UnlinkedBookData struct {
ProgressID string
DeviceID string
DeviceType string
DeviceName string
FilePath string
SHA256 string
TitleFromDevice string
LastSyncTime string
ConfidenceScore float64
PotentialMatches []PotentialMatchData
}
type PotentialMatchData struct {
MediaItemID string
Title string
Author string
CoverImagePath string
MatchMethod string
Confidence float64
}
// Documentation types
type Navigation struct {
Sections []NavSection
}
type NavSection struct {
Title string
Items []NavItem
Collapsed bool
}
type NavItem struct {
Title string
URL string
Icon string
}
type Document struct {
Title string
Content string
TOC []TOCItem
Breadcrumb []BreadcrumbItem
Category string
SourceFile string
}
type TOCItem struct {
Level int
Title string
Anchor string
}
type BreadcrumbItem struct {
Title string
URL string
}
// UnsafeHTML is used for rendering raw HTML (like rendered markdown)
type UnsafeHTML string
// ToComponent converts UnsafeHTML to a templ Component
func (u UnsafeHTML) ToComponent() templ.Component {
return templ.Raw(string(u))
}
// Reader types
type ReaderMetadata struct {
MediaItemID string `json:"media_item_id"`
Title string `json:"title"`
Author string `json:"author"`
CoverImagePath string `json:"cover_image_path"`
LibraryType string `json:"library_type"`
MimeType string `json:"mime_type"`
FilePath string `json:"file_path"`
TotalPages int `json:"total_pages"`
ChapterCount int `json:"chapter_count"`
FormatGroup string `json:"format_group"`
MangaType string `json:"manga_type"`
ReadingDirection string `json:"reading_direction"`
FileURL string `json:"file_url"`
LibraryID string `json:"library_id"`
TotalCharacters int64 `json:"total_characters"`
EstimatedPages int `json:"estimated_pages"`
// TreatAsComic: fixed-layout item in a comics/manga library gets
// comic reader treatment (bookmarks only — no highlights, no search).
TreatAsComic bool `json:"treat_as_comic"`
}
type ReadingProgress struct {
ID string
MediaItemID string
UserID string
CurrentPage int
TotalPages int
Percentage float64
EpubCfi string
LastReadAt time.Time
Chapter int
ChapterProgress float64
FormatGroup string
}
type Bookmark struct {
ID string
MediaItemID string
UserID string
PageNumber *int
ChapterNumber *int
CfiPosition string
Title string
Position string
Notes string
CreatedAt time.Time
}
// ProcessingIssueData - Processing issue types
type ProcessingIssueData struct {
ID string
MediaItemID string
Title string
FilePath string
FormatGroup string
LibraryTypeName string
IssueType string
IssueDescription string
Severity string
CreatedAt time.Time
}
type IssueStats struct {
ErrorCount int64
WarningCount int64
InfoCount int64
}