The landing screen now reads as a product rather than a list.
- migration 0006: item_views, per user. "Most viewed" is personal — what I
re-read is not what you re-read. Counted when an item is opened in a reader,
so the number means consumed, not merely fetched.
- Suggestions: unstarted items related to what the user has been consuming —
same author first, then same kind, then recent as a floor. Each carries the
reason it was chosen ("More by Daft Punk"), because a suggestion nobody can
understand is noise.
- Explore: a mixed sample across every kind, excluding in-progress items so the
zone is discovery rather than a repeat.
- Home fetches all five shelves in one request; the screen never pops in.
- web: Shelf gained subtitle/badge support, Card gained a subtitle, and the
home renders four shelves plus a wrapping Explore grid.
Three bugs found and fixed while verifying:
- Continue showed a multi-track audiobook once per chapter. GROUP BY item_id
with MAX(updated_at) picks one card per item (SQLite takes the bare columns
from the row that produced the max).
- Suggestions and Explore passed their SQL arguments in the wrong order: the IN
list precedes the user id in those queries, so a user id was being bound as a
library id. Covered by a scoping test that would have caught it.
- The sidebar lost its media types on a cold, logged-out load. bootstrap()
fetched media types inside a Promise.all with /auth/me, so the expected 401
rejected the whole thing and nothing refetched after login. Media types are
public and now load independently.
18 lines
840 B
SQL
18 lines
840 B
SQL
-- 0006_views.sql — per-user view counts.
|
|
--
|
|
-- "Most Viewed" is a per-user notion, not a household one: what I re-read is
|
|
-- not what you re-read. Counted when an item is actually opened in a reader,
|
|
-- not when its detail page is fetched, so the number means "consumed".
|
|
--
|
|
-- Only the current view/play count and the last time are stored; a full event
|
|
-- log would grow without bound for a feature this simple.
|
|
|
|
CREATE TABLE item_views (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
views INTEGER NOT NULL DEFAULT 0,
|
|
last_viewed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
PRIMARY KEY (user_id, item_id)
|
|
);
|
|
|
|
CREATE INDEX idx_item_views_user ON item_views(user_id, views DESC);
|