Follows: followed tags/pools, feeds, unseen badges and blacklist cloud

Backend (new apps.follows):
- FollowedTag/FollowedPool/FollowedPost models; per-user follows with
  unseen tracking, plus FollowCloud for the cached blacklist cloud.
- Two periodic commands sharing one fetch path: sync_followed_tags and
  sync_followed_pools fetch each followed tag/pool's newest posts (one
  e621 search per unique follow), store unseen feed rows, refresh covers
  and pool metadata; both fall back to anonymous e621 access.
- API: /api/follows/tags|pools (follow, unfollow, mark seen), a merged
  feed with per-follow filtering, and /api/follows/cloud/ which rebuilds
  the blacklisted-tag cloud in a daemon thread when its 10 min cache is
  stale (polling returns building/ready).
- e621 client now supports anonymous reads; trimmed posts carry preview
  URLs for covers and feed tiles.

Frontend:
- /followed page: follow forms, cover cards with unseen badges and
  Mark seen, merged feed with filter/unseen toggle, and a blacklist
  cloud panel that polls while building. Followed nav entry added.
This commit is contained in:
2026-09-17 14:09:30 -05:00
parent c55fa1fdca
commit 03dd235f3a
23 changed files with 1741 additions and 6 deletions
+71
View File
@@ -204,6 +204,77 @@ export interface MatchTask {
updated_at: string;
}
export interface FollowCoverData {
post_id: number | null;
rating: string | null;
preview_url: string | null;
score: number | null;
fav_count: number | null;
}
export interface FollowedTag {
id: number;
tag: string;
unseen_count: number;
cover_data: FollowCoverData | null;
last_synced_at: string | null;
created_at: string;
}
export interface FollowedPool {
id: number;
pool_id: number;
name: string;
post_count: number;
unseen_count: number;
cover_data: FollowCoverData | null;
last_synced_at: string | null;
created_at: string;
}
export interface FollowFeedSource {
kind: "tag" | "pool";
id: number;
label: string;
}
export interface FollowFeedEntry {
post_id: number;
seen: boolean;
data: {
id?: number | null;
rating?: string | null;
tags?: Record<string, string[]>;
score?: { total?: number | null } | null;
fav_count?: number | null;
file?: {
width?: number | null;
height?: number | null;
ext?: string | null;
} | null;
preview?: { url?: string | null } | null;
};
sources: FollowFeedSource[];
}
export interface FollowFeed {
count: number;
page: number;
per_page: number;
has_next: boolean;
results: FollowFeedEntry[];
}
export interface FollowCloud {
status: "ready" | "building";
stale: boolean;
tags: [string, number][];
posts: number;
source: "user" | "default" | null;
blacklist_count: number;
computed_at: string | null;
}
export interface E621IqdbCandidate {
post_id: number | null;
score: number | null;