schema(dashboard): implement Phase 1 unified collections architecture

Add support for Carousel-style dashboard with unified collections architecture:

Database Schema Changes:
- Add user_dashboard_preferences table:
  - hidden_collections: TEXT[] for managing section visibility
  - collection_order: TEXT[] for custom ordering
  - items_per_section: INT for limiting items per section
- Update collections table:
  - user_id: Make nullable to support system-owned collections (NULL = system)
  - show_on_dashboard: BOOLEAN for controlling visibility
  - query_type: TEXT for different query types (continue-reading, recently-added, etc.)
  - priority: INT for display order (lower = higher priority)
  - is_system_collection: BOOLEAN for flagging system defaults
- Update collection_items table:
  - Add excluded BOOLEAN for user overrides of auto-assigned items

Indexes:
- idx_collections_dashboard: (user_id, show_on_dashboard, priority) WHERE show_on_dashboard = true
- idx_dashboard_prefs_user_library: (user_id, library_id)
- idx_collection_items_excluded: (collection_id, excluded) WHERE excluded = true

System Collections (pre-seeded defaults):
- continue-reading: Books with 0 < progress < 1
- recently-added: Newly added items to library
- recently-read: Books with progress >= 1
- not-started: Books with progress = 0 or no record

This implements Phase 1 of the Carousel-style dashboard redesign plan.
This commit is contained in:
2026-02-19 20:52:21 -05:00
parent ceca81f098
commit 3af2fb0ba4
3 changed files with 113 additions and 28 deletions
+25 -9
View File
@@ -14,18 +14,23 @@ type CollectionItems struct {
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
AddedByUserID pgtype.UUID `db:"added_by_user_id" json:"added_by_user_id"`
Excluded pgtype.Bool `db:"excluded" json:"excluded"`
}
type Collections struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
QueryType pgtype.Text `db:"query_type" json:"query_type"`
Priority pgtype.Int4 `db:"priority" json:"priority"`
IsSystemCollection pgtype.Bool `db:"is_system_collection" json:"is_system_collection"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type DeviceCatalogs struct {
@@ -373,6 +378,17 @@ type UnlinkedBooks struct {
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type UserDashboardPreferences struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
HiddenCollections []string `db:"hidden_collections" json:"hidden_collections"`
CollectionOrder []string `db:"collection_order" json:"collection_order"`
ItemsPerSection pgtype.Int4 `db:"items_per_section" json:"items_per_section"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type Users struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
+49 -18
View File
@@ -16,7 +16,7 @@ const AddBookToCollection = `-- name: AddBookToCollection :one
INSERT INTO collection_items (collection_id, media_item_id, added_by_user_id)
VALUES ($1, $2, $3)
ON CONFLICT (collection_id, media_item_id) DO NOTHING
RETURNING id, collection_id, media_item_id, added_at, added_by_user_id
RETURNING id, collection_id, media_item_id, added_at, added_by_user_id, excluded
`
type AddBookToCollectionParams struct {
@@ -36,6 +36,7 @@ func (q *Queries) AddBookToCollection(ctx context.Context, arg AddBookToCollecti
&i.MediaItemID,
&i.AddedAt,
&i.AddedByUserID,
&i.Excluded,
)
return i, err
}
@@ -232,7 +233,7 @@ const CreateCollection = `-- name: CreateCollection :one
INSERT INTO collections (user_id, name, description, color, icon, auto_assign_rules, view_settings)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, view_settings, created_at
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at
`
type CreateCollectionParams struct {
@@ -267,6 +268,10 @@ func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionPara
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
)
return i, err
@@ -1542,7 +1547,7 @@ func (q *Queries) GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsF
}
const GetCollection = `-- name: GetCollection :one
SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, created_at FROM collections WHERE id = $1
SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at FROM collections WHERE id = $1
`
// Get collection
@@ -1558,13 +1563,17 @@ func (q *Queries) GetCollection(ctx context.Context, id pgtype.UUID) (Collection
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
)
return i, err
}
const GetCollectionItems = `-- name: GetCollectionItems :many
SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_id, mi.title, mi.author, mi.cover_image_path
SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_id, ci.excluded, mi.title, mi.author, mi.cover_image_path
FROM collection_items ci
JOIN media_items mi ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1
@@ -1577,6 +1586,7 @@ type GetCollectionItemsRow struct {
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
AddedByUserID pgtype.UUID `db:"added_by_user_id" json:"added_by_user_id"`
Excluded pgtype.Bool `db:"excluded" json:"excluded"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
@@ -1598,6 +1608,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU
&i.MediaItemID,
&i.AddedAt,
&i.AddedByUserID,
&i.Excluded,
&i.Title,
&i.Author,
&i.CoverImagePath,
@@ -1614,7 +1625,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU
const GetCollectionWithBookCount = `-- name: GetCollectionWithBookCount :one
SELECT
c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.created_at,
c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at,
COUNT(ci.id) as book_count
FROM collections c
LEFT JOIN collection_items ci ON c.id = ci.collection_id
@@ -1623,16 +1634,20 @@ GROUP BY c.id
`
type GetCollectionWithBookCountRow struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
BookCount int64 `db:"book_count" json:"book_count"`
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
QueryType pgtype.Text `db:"query_type" json:"query_type"`
Priority pgtype.Int4 `db:"priority" json:"priority"`
IsSystemCollection pgtype.Bool `db:"is_system_collection" json:"is_system_collection"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
BookCount int64 `db:"book_count" json:"book_count"`
}
// Get collection with book count
@@ -1648,6 +1663,10 @@ func (q *Queries) GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
&i.BookCount,
)
@@ -1655,7 +1674,7 @@ func (q *Queries) GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID
}
const GetCollectionsByUser = `-- name: GetCollectionsByUser :many
SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, created_at FROM collections WHERE user_id = $1 ORDER BY created_at DESC
SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at FROM collections WHERE user_id = $1 ORDER BY created_at DESC
`
// Get collections by user
@@ -1677,6 +1696,10 @@ func (q *Queries) GetCollectionsByUser(ctx context.Context, userID pgtype.UUID)
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
); err != nil {
return nil, err
@@ -1690,7 +1713,7 @@ func (q *Queries) GetCollectionsByUser(ctx context.Context, userID pgtype.UUID)
}
const GetCollectionsForBook = `-- name: GetCollectionsForBook :many
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.created_at
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at
FROM collections c
JOIN collection_items ci ON c.id = ci.collection_id
WHERE ci.media_item_id = $1
@@ -1715,6 +1738,10 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
); err != nil {
return nil, err
@@ -6483,7 +6510,7 @@ SET
auto_assign_rules = $6,
view_settings = $7
WHERE id = $1
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, view_settings, created_at
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at
`
type UpdateCollectionParams struct {
@@ -6517,6 +6544,10 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara
&i.Icon,
&i.AutoAssignRules,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
)
return i, err