From c65da42593043decd229118073e62718c313dc6d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 6 Jun 2026 20:34:15 -0400 Subject: [PATCH 1/3] fix(patreon): handle the /cw/ creator-URL prefix in vanity extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A source URL like https://www.patreon.com/cw/Atole resolved vanity='cw' — the vanity regex only skipped a /c/ prefix, so Patreon's current /cw/ ("creator workspace") form fell through to the bare-vanity branch and captured the prefix instead of the slug. Every /cw/ source then failed campaign-id resolution (API + page-scrape both looked up "cw"). Operator-confirmed 2026-06-07 via the new error text: source_url='.../cw/Atole'; vanity='cw'. Add cw/ to the optional prefix group (ordered before c/ so the longer prefix wins), and have the creator-page fallback try the /cw/ form too. Test covers bare / c/ / cw/ extraction and that id: URLs stay non-vanity. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/patreon_resolver.py | 20 +++++++++++++++----- tests/test_patreon_resolver.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/app/services/patreon_resolver.py b/backend/app/services/patreon_resolver.py index c8acd2c..8c8d963 100644 --- a/backend/app/services/patreon_resolver.py +++ b/backend/app/services/patreon_resolver.py @@ -30,9 +30,17 @@ _CAMPAIGNS_URL = "https://www.patreon.com/api/campaigns" # A source URL of the form `.../id:` already carries the campaign id # (no lookup needed). The vanity regex deliberately EXCLUDES the id: form so the # two paths don't overlap. +# +# Patreon serves the creator vanity under several path prefixes — bare +# (`patreon.com/Atole`), `c/` (`patreon.com/c/Atole`), and `cw/` +# (`patreon.com/cw/Atole`, its current "creator workspace" URL). The optional +# prefix group must list `cw/` BEFORE `c/` so the longer prefix wins — otherwise +# `cw/Atole` matches the bare branch and yields vanity="cw" (operator-flagged +# 2026-06-07: every `/cw/` source failed resolution on vanity="cw"). _ID_URL_RE = re.compile(r"/id:(\d+)") +_VANITY_PREFIXES = ("cw/", "c/") _VANITY_RE = re.compile( - r"^https?://(?:www\.)?patreon\.com/(?:c/)?(?!id:)([^/?#]+)", + r"^https?://(?:www\.)?patreon\.com/(?:cw/|c/)?(?!id:)([^/?#]+)", re.IGNORECASE, ) _USER_AGENT = ( @@ -141,10 +149,12 @@ def _lookup_via_page(vanity: str, cookies_path: str | None) -> str | None: Patreon redirects between. Never raises.""" jar = _load_cookie_jar(cookies_path) headers = {"User-Agent": _USER_AGENT, "Accept": "text/html"} - for page_url in ( - f"https://www.patreon.com/{vanity}", - f"https://www.patreon.com/c/{vanity}", - ): + # Try the bare vanity and every known creator-path prefix Patreon + # redirects between (c/, cw/) — the one the source used isn't known here + # (extract_vanity already stripped it). + page_urls = [f"https://www.patreon.com/{vanity}"] + page_urls += [f"https://www.patreon.com/{p}{vanity}" for p in _VANITY_PREFIXES] + for page_url in page_urls: try: resp = requests.get( page_url, diff --git a/tests/test_patreon_resolver.py b/tests/test_patreon_resolver.py index 6586e41..3b2e226 100644 --- a/tests/test_patreon_resolver.py +++ b/tests/test_patreon_resolver.py @@ -169,3 +169,17 @@ async def test_for_source_unresolvable_returns_none(): cid, resolved = await resolve_campaign_id_for_source("not-a-patreon-url", None, {}) assert cid is None assert resolved is None + + +def test_extract_vanity_handles_all_path_prefixes(): + """Patreon serves the vanity bare and under c/ and cw/ prefixes; all must + yield the creator slug, not the prefix (operator-flagged 2026-06-07: a + /cw/Atole source resolved vanity='cw').""" + from backend.app.services.patreon_resolver import extract_vanity + + assert extract_vanity("https://www.patreon.com/Atole") == "Atole" + assert extract_vanity("https://www.patreon.com/c/Atole") == "Atole" + assert extract_vanity("https://www.patreon.com/cw/Atole") == "Atole" + assert extract_vanity("https://patreon.com/cw/Atole") == "Atole" + # id: URLs are NOT vanities (handled by the id path). + assert extract_vanity("https://www.patreon.com/id:123") is None From b7d07324eee4235cbcea219701fd3c602090b2a5 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 6 Jun 2026 20:48:01 -0400 Subject: [PATCH 2/3] fix(subs): stop the lock/reload on source actions + regroup the row buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock/reload: every inline source action (check / backfill / recover / toggle / remove) ended by refetching the WHOLE subscription list (store.loadAll / refresh), which blocked the UI and re-rendered the table — collapsing the expanded row, which read as "locks then resets." The action APIs already return the updated source, so the store now patches that one row in place (_patchSource / _dropSource); the post-action loadAll/refresh calls are gone. Toggling enabled, starting/stopping a backfill, recovering, and removing are now instant and leave the expansion intact. Button regroup (operator-flagged: tiny, mis-clickable, not grouped by function): - New shared SourceActions.vue used by desktop SourceRow + mobile SourceCard. - Frequent actions stay as size="small" buttons: Check, Backfill/Stop. - Low-frequency / destructive actions move into a labelled overflow (⋮) menu — Preview backfill, Recover dropped near-duplicates, Remove source — so they can't be fat-fingered, and the labels spell out recover-vs-backfill. - Edit moves next to the source URL (its identity), out of the action cluster where it sat beside Remove. - Single-source Remove now confirms (it had no guard before). Tests: store patches/drops in place without dropping the cache. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../subscriptions/SourceActions.vue | 91 +++++++++++++++++ .../components/subscriptions/SourceCard.vue | 80 +++++---------- .../components/subscriptions/SourceRow.vue | 98 +++++++------------ .../subscriptions/SubscriptionsTab.vue | 25 +++-- frontend/src/stores/sources.js | 35 ++++++- frontend/test/sources.spec.js | 35 +++++++ 6 files changed, 238 insertions(+), 126 deletions(-) create mode 100644 frontend/src/components/subscriptions/SourceActions.vue diff --git a/frontend/src/components/subscriptions/SourceActions.vue b/frontend/src/components/subscriptions/SourceActions.vue new file mode 100644 index 0000000..94b2a02 --- /dev/null +++ b/frontend/src/components/subscriptions/SourceActions.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/frontend/src/components/subscriptions/SourceCard.vue b/frontend/src/components/subscriptions/SourceCard.vue index 0e0617b..c8f5fb2 100644 --- a/frontend/src/components/subscriptions/SourceCard.vue +++ b/frontend/src/components/subscriptions/SourceCard.vue @@ -12,10 +12,19 @@ /> - {{ source.url }} +
+ {{ source.url }} + + mdi-pencil + Edit source + +
Last {{ formatRelative(source.last_checked_at) }} @@ -42,61 +51,20 @@
- - mdi-play - Check now - - - {{ source.backfill_state === 'running' ? 'mdi-stop' : 'mdi-magnify-scan' }} - - {{ source.backfill_state === 'running' - ? (source.backfill_bypass_seen ? 'Stop recovery' : 'Stop backfill') - : 'Backfill full history' }} - - - - mdi-eye-outline - - Preview — count what a backfill would download (no download) - - - - mdi-backup-restore - - Recover — re-fetch dropped near-dups & re-evaluate under the current threshold - - - - mdi-pencil - Edit - - - mdi-close - Remove - +