feat(artist): "new since last visit" badge + banner
Per-artist "+N" accent pill on the artists directory and a "N new since last visit" banner inside ArtistView. Counts new IMAGES (not posts) so multi-image posts increment correctly. - alembic 0034: artist_visit (artist_id PK, last_viewed_at NOT NULL). Seeds every existing artist with last_viewed_at=NOW() so the badge starts at 0 across the board — no noisy "5000 unseen images" on first deploy. - ArtistService.find_or_create autoseeds a visit row alongside new artists, so freshly imported content doesn't read as unseen. - ArtistService.overview reads pre-visit last_viewed_at, counts images created since, then atomically UPSERTs last_viewed_at=NOW() via postgres ON CONFLICT DO UPDATE (no SELECT-then-INSERT race per reference_scalar_one_or_none_duplicates). Returns the pre-update count as `unseen_count_at_visit` so the banner has data. - ArtistDirectoryService.list_artists adds an `unseen_count` aggregate to each card via LEFT JOIN artist_visit + conditional COUNT. NULL last_viewed_at (artist created before this code shipped) defensively counts as "never visited" → all images unseen. - Frontend: ArtistCard renders an accent pill in the preview-strip corner when unseen_count > 0 (capped at 99+); ArtistView shows a closable v-alert banner on initial load when unseen_count_at_visit > 0, re-arms on slug change. Single-row-per-artist (no user_id) — rule #47 multi-user ACL is aspirational; widens to (user_id, artist_id) PK when User lands, per rule #22. Scribe plan #597.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from .app_setting import AppSetting
|
||||
from .artist import Artist
|
||||
from .artist_visit import ArtistVisit
|
||||
from .backup_run import BackupRun
|
||||
from .base import Base
|
||||
from .credential import Credential
|
||||
@@ -28,6 +29,7 @@ __all__ = [
|
||||
"Base",
|
||||
"AppSetting",
|
||||
"Artist",
|
||||
"ArtistVisit",
|
||||
"BackupRun",
|
||||
"Source",
|
||||
"Credential",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""ArtistVisit — per-artist 'last viewed' timestamp.
|
||||
|
||||
Powers the "+N new since last visit" badge on the artists directory and
|
||||
the matching banner on `ArtistView`. One row per artist, single global
|
||||
operator. When the multi-user model lands, the PK widens to
|
||||
`(user_id, artist_id)` — currently aspirational only (no User model,
|
||||
no services/access.py); operator approved skipping `user_id` for now
|
||||
under rule #22 (breaking changes welcome).
|
||||
|
||||
Seed at migration time: every existing artist gets `last_viewed_at = NOW()`
|
||||
so the badge starts at 0 across the board (no noisy "5000 unseen" on
|
||||
first deploy). New artists also auto-get a row via
|
||||
`ArtistService.find_or_create`.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class ArtistVisit(Base):
|
||||
__tablename__ = "artist_visit"
|
||||
|
||||
artist_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("artist.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
last_viewed_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
)
|
||||
Reference in New Issue
Block a user