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.
54 lines
1.6 KiB
Python
54 lines
1.6 KiB
Python
"""artist_visit: per-artist last-viewed timestamp for the "+N new" badge
|
|
|
|
Revision ID: 0034
|
|
Revises: 0033
|
|
Create Date: 2026-06-03
|
|
|
|
Powers the artists-directory "+N new since last visit" badge + ArtistView
|
|
banner. Single row per artist (no user_id yet — rule #47 multi-user ACL
|
|
is aspirational; widens to (user_id, artist_id) PK when User lands).
|
|
|
|
Seed every existing artist with `last_viewed_at = NOW()` so the badge
|
|
starts at 0 across the board — no noisy "you have 5000 unseen images"
|
|
on first deploy. New artists auto-get a row via
|
|
`ArtistService.find_or_create`.
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0034"
|
|
down_revision: Union[str, None] = "0033"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"artist_visit",
|
|
sa.Column(
|
|
"artist_id",
|
|
sa.Integer,
|
|
sa.ForeignKey("artist.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
),
|
|
sa.Column(
|
|
"last_viewed_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("NOW()"),
|
|
),
|
|
)
|
|
# Seed: every existing artist starts "fully caught up". Without this,
|
|
# every operator with N artists would see N badges (worth of every
|
|
# image ever imported) on first deploy.
|
|
op.execute(
|
|
"INSERT INTO artist_visit (artist_id, last_viewed_at) "
|
|
"SELECT id, NOW() FROM artist"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("artist_visit")
|