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.
310 lines
6.7 KiB
Go
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
|
|
}
|