Lapsed subscriptions stop pulling, a quieter Subscriptions card, and working feed filters #254

Merged
bvandeusen merged 3 commits from dev into main 2026-09-13 22:45:04 -04:00
3 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 3fe9d0a612 fix: the Latest feed's filter dropdowns opened empty
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m2s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m20s
Operator: "the filters in the latest feed, drop down but don't have values". Two separate causes:

- Platform: PostsFilterBar built its items from `platformsStore.platforms`. The platforms store has never had that property; it exposes `list` and `byKey`. The read returned undefined, `|| []` turned that into an empty list, and nothing failed. ArtistsView had copied the same read, so the Browse → Artists platform filter was empty too. Both now read `list` and show platform names rather than raw keys.

- Artist: the autocomplete searched the server only after something was typed (autocomplete returns [] for an empty query by design, which its tests pin). Opening the dropdown therefore showed an empty menu. PostsFilterBar now loads every artist once from a new lightweight `GET /api/artists/names` (id, name, slug; alphabetical; no joins) and filters client-side, so the list is there on open. A deep-linked artist_id now also shows the artist's real name instead of "Artist #id".

Guard: frontend/test/storeUsage.spec.js scans src for `platformsStore.<name>` and fails on any name the store doesn't define, since the frontend CI has no type-checker to catch this. A positive control shows the shipped `platformsStore.platforms` read is flagged, and a vacuity check confirms the scan really walks the tree.

tests/test_api_artists_create.py covers /names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
2026-09-13 22:38:27 -04:00
bvandeusenandClaude Opus 5 7ff8915147 feat: a source stops pulling once its membership ends, and resumes on resubscribe (3995)
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 27s
CI / backend-lint-and-test (push) Successful in 34s
Build images / build-web (push) Successful in 1m24s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m52s
Build images / promote (push) Skipped
CI / integration (push) Successful in 3m1s
Operator, 2026-09-13: "if I kill a subscription on patreon I would like the pulling to stop on curator as well", with auto-resume chosen. This reverses the 2026-09-11 "report only" decision for lapsed sources.

membership_reconcile.apply_membership_lapses runs in sync_memberships right after each platform's successful sync, so it only ever acts on the roster just written.

It stops a source (enabled=false, with the same failure-state reset as a manual disable, #1285) only when all of these hold:
- the roster is fresh
- the source's matched membership says has_paid_access is False (lapsed, or a free follow)
- the paid-through date has passed, where the platform gives one (Patreon's member.access_expires_at; SubscribeStar gives none, so it stops at once)
- the source is enabled
- the operator hasn't chosen to keep it

It never acts on absence. A source with no matched membership keeps pulling, because a rename or a never-walked source produces the same absence. An unrecognised status is never a lapse either.

It resumes only sources carrying its own `_membership_stopped` marker, once the membership is paid again.

The operator outranks the sweep both ways (SourceService.update):
- turning a stopped source back on marks it `_membership_kept`, so the next sweep leaves it alone until it's paid again
- turning a source off by hand drops the marker, so the sweep never switches it back on

Both are `_`-prefixed app-managed config keys, which operator edits already preserve. No migration.

The roster/fetch line holds. This is a source-level action by the sweep. No download path reads the roster, and the scheduler still selects on `enabled` alone. test_no_fetch_path_can_read_the_roster is unchanged.

UI: SourceRow shows a neutral "Membership ended" chip, with the status and the resume/keep explanation, ahead of the other chips. The sweep's task summary reports stopped/resumed counts.

Tests (tests/test_membership_lapses.py):
- a lapse stops the source with a clean slate and keeps the id cache
- paid-through is honoured
- absence, an unknown status and a stale roster never stop anything
- a resume touches only what the sweep stopped
- a manual on sticks, a manual off drops the marker, and a kept source is released once paid

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
2026-09-13 22:08:06 -04:00
bvandeusenandClaude Opus 5 4d84ab2816 fix: the filter rail goes, and the subscriptions card stops listing what you don't pay for — and can be dismissed
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 7s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m8s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m29s
Operator live review of #253 on the wide window. The images landed well, but:

- "the left rail feels like wasted space". The filters and check status move back into one row above the feed (PostsView). On a wide window the row lines up with the feed column, clear of the day gutter, and the container recentres without the 280px rail. The narrow layout is as before, with the status still first. This retires #407 option E; A and D stay.

- The "you can't see" link landed on the Subscriptions reconcile card, which listed 9 sources as `former_patron` under "sources your roster doesn't account for". The operator doesn't want sources they no longer pay for listed there, and wants the card dismissable "unless something changes":
  - MembershipReconcileCard renders only "creators you subscribe to but don't follow here" and the stale-roster warning.
  - Stopping pulls on lapsed sources is the membership sweep's job instead (#3995, next).
  - A close button dismisses the card. The dismissal is keyed to a fingerprint of what the card says (offered membership ids plus stale-roster flags), so a new subscription or a roster going stale brings it back. It's stored per browser, which is enough for a single-operator instance.

The backend still computes tracked_not_subscribed. #3995 acts on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
2026-09-13 22:04:02 -04:00