fix(e8): deep-review hardening + cut 0.73.0

Fixes from several multi-agent deep-review passes over the E8 feed-filtering &
channel-manager epic (S1-S4):
- feed_years buckets by UTC year to match the UTC-pinned year-filter bounds
- tag suggestions: examples ∩ subscribed (drop tagged-then-unsubscribed channels
  from the profile denominator); exclude hidden channels as suggestion targets
- channel manager: restore ghost chips + surface an error toast on a failed tag accept
- year filter: debounce the search term so the per-year facet doesn't refetch per keystroke
- channel tags: aria-label the "+N" overflow pills
- extract useTagSuggestions hook; add backend + hook tests (profile boundaries, cap,
  language exclusion, unsubscribed/hidden handling, UTC bucketing, bulk-accept)

Bump VERSION to 0.73.0 + release notes.
This commit is contained in:
2026-08-26 05:25:47 +02:00
parent 17f8885fb1
commit 7104767ff9
13 changed files with 519 additions and 70 deletions
+5 -1
View File
@@ -450,7 +450,11 @@ def feed_years(db: Session, user: User, **params) -> list[dict]:
**{**params, "published_after": None, "published_before": None, "max_age_days": None},
)
pub = base.with_only_columns(Video.published_at.label("pub")).subquery()
year = func.extract("year", pub.c.pub)
# Bucket by UTC year to stay in lockstep with the year filter's boundaries (published_after/before
# are pinned to UTC in filtered_query). published_at is timestamptz, so a bare extract('year', …)
# would use the DB session timezone and, on a non-UTC session, bucket a video near a year boundary
# into a different year than the filter includes — the count and the drill-down feed would disagree.
year = func.extract("year", func.timezone("UTC", pub.c.pub))
rows = db.execute(
select(year.label("y"), func.count()).group_by(year).order_by(year.desc())
).all()
+18 -8
View File
@@ -51,17 +51,25 @@ def compute_tag_suggestions(db: Session, user: User) -> dict[str, list[dict]]:
capped at ``MAX_PER_CHANNEL``. Channels with no suggestion are omitted (empty dict when the
user has no tag with a usable profile). Deterministic for a fixed DB state.
"""
# Candidate universe: the channels this user manages. A user can only tag a subscribed channel
# (attach_tag requires a subscription), so example channels are a subset of this too — which
# lets us bound the auto-tag scan to just these channels.
subscribed: set[str] = set(
db.scalars(select(Subscription.channel_id).where(Subscription.user_id == user.id)).all()
)
# Candidate universe: the channels this user is subscribed to. `subscribed` (incl. hidden) is the
# SIGNAL set — a hidden channel is still a valid example / topic-feature source, since hiding only
# affects the feed, not the tagging pattern. `hidden` is excluded as a suggestion TARGET below (the
# Channel Manager shows ghost chips only on non-hidden rows, so suggesting for hidden channels would
# inflate the header count past what's shown and let bulk-accept tag channels the user can't see).
subs = db.execute(
select(Subscription.channel_id, Subscription.hidden).where(Subscription.user_id == user.id)
).all()
subscribed: set[str] = {cid for cid, _ in subs}
hidden: set[str] = {cid for cid, h in subs if h}
if not subscribed:
return {}
# The user's manual tag links: tag_id -> example channels, and each channel's user-tag set (so
# a channel with ANY user tag is excluded from "untagged").
# a channel with ANY user tag is excluded from "untagged"). Intersect with `subscribed`: unsubscribe
# deletes the Subscription but NOT the source='user' ChannelTag, so a tagged-then-unsubscribed
# channel would otherwise stay an "example" — inflating a profile's CHAR_SUPPORT denominator while
# contributing zero features (features are scanned over `subscribed` only), dropping characteristic
# topics. Keeping examples ⊆ subscribed makes the support fraction honest.
examples: dict[int, set[str]] = {}
user_tagged: set[str] = set()
for tag_id, cid in db.execute(
@@ -69,6 +77,8 @@ def compute_tag_suggestions(db: Session, user: User) -> dict[str, list[dict]]:
ChannelTag.user_id == user.id, ChannelTag.source == "user"
)
).all():
if cid not in subscribed:
continue
examples.setdefault(tag_id, set()).add(cid)
user_tagged.add(cid)
@@ -104,7 +114,7 @@ def compute_tag_suggestions(db: Session, user: User) -> dict[str, list[dict]]:
suggestions: dict[str, list[dict]] = {}
for cid in subscribed:
if cid in user_tagged: # already has a user tag → not "untagged"
if cid in hidden or cid in user_tagged: # hidden → no chips shown; tagged → not "untagged"
continue
cfeat = features.get(cid)
if not cfeat: