b65e956ad2
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.
37 lines
1.2 KiB
Python
37 lines
1.2 KiB
Python
"""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(),
|
|
)
|