Skip to content

feat(afdian): add sponsor thank-you page to sidebar nav (AFD-004) - #7

Merged
everbird merged 6 commits into
mainfrom
afdian-wave2
Aug 4, 2026
Merged

feat(afdian): add sponsor thank-you page to sidebar nav (AFD-004)#7
everbird merged 6 commits into
mainfrom
afdian-wave2

Conversation

@everbird

@everbird everbird commented Aug 4, 2026

Copy link
Copy Markdown
Owner

No description provided.

everbird and others added 6 commits August 4, 2026 02:17
…view

AFD-001 review pass on b1728f5: single-source URL config, external-link
rel/target, SUP-004 copy, and keyboard/a11y all check out. Site is
dark-only (no prefers-color-scheme / data-theme), so button reuses the
existing --app-* vars consistently.

Only nit: .donate-afdian-btn used var(--app-text-bright) with no
fallback while --app-text-bright is undefined site-wide; align it with
the existing var(--app-text-bright, #fff) usage in the same file.

Docs: assign AFD-001 Review AI, add changelog WAVE-20260804-02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
…FD-002)

Add ohmywod/afdian.py: a tiny stdlib-only (urllib/hashlib/json, no new
dependency) read-only client for the sponsor wall. sign() implements
md5(token+"params"+params+"ts"+ts+"user_id"+user_id); query_sponsor()
POSTs query-sponsor with a 5s timeout and translates every network / HTTP
/ decode / ec!=200 failure into AfdianError without leaking the token;
fetch_all_sponsors() paginates under a MAX_PAGES bound. Credentials come
from Flask config (AFDIAN_USER_ID / AFDIAN_TOKEN); empty or unrendered
"<secret:...>" values are treated as unconfigured so dev/unprovisioned
environments degrade to an empty wall instead of raising.

config.py DefaultConfig gains the two keys as an empty schema reference
(runtime uses local_config.py, rendered by ohmywod-ops).

tests/test_afdian.py (8): signing vector, unconfigured->empty, placeholder
rejection, pagination, MAX_PAGES bound, and two token-non-leak error paths.
Full suite 120 passed.

Docs: AFD-002 assessing -> in_progress with scaffold evidence, changelog
WAVE-20260804-03. Real-credential query still pending to reach done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
…2 done

Real query-sponsor call with provisioned credentials revealed afdian.com
sits behind Cloudflare, which 403-bans the default Python-urllib UA with
"error code: 1010". Send a browser User-Agent (and Accept: application/json)
so the API answers; afdian.com is the correct host (afdian.net no longer
resolves). Verified end-to-end: HTTP 200, ec=200, signature accepted,
is_configured()=True, fetch_all_sponsors()=[] (account has 0 sponsors).

Add test_query_sponsor_sends_browser_user_agent to guard the regression;
test_afdian 9 pass, full suite 121 pass.

Docs: AFD-002 in_progress -> done with real-call evidence, changelog
WAVE-20260804-04.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
Add get_sponsors(): serves a fresh Redis copy (AFDIAN_CACHE_TTL, default
1h) on hit; on miss pulls from Afdian and refreshes both a fresh (TTL) and
a long-lived last-good copy. When Afdian is unreachable it degrades to the
last-good copy, else an empty list — logs a warning, never raises, never
blocks the page. Afdian stays the single source of truth: no SQLite writes,
no models/migrations, nothing enters the litestream/DR chain.

config.py gains AFDIAN_CACHE_TTL (overridable). tests/test_afdian.py adds 4
cache cases (miss->fetch->hit, degrade-to-last-good, degrade-to-empty,
unconfigured->empty); test_afdian 13 pass, full suite 122.

Docs: AFD-003 todo -> done, changelog WAVE-20260804-05.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
Privacy-first, nickname-only wall completing Wave 2. Per the chosen policy:
show only the Afdian nickname (no amount, no tier, no id/avatar); a sponsor
with a blank/missing/malformed nickname is shown as "匿名支持者"; the wall
lives on a standalone /thanks page linked from the landing support panel.

- afdian.sponsor_display_names() maps the AFD-003 cached source to display
  nicknames, anonymising anything without a usable name and never raising.
- frontend.thanks_page (GET /thanks) renders thanks.html: nickname chips or
  a restrained empty state; copy states nickname-only / no amount and does
  not imply support buys features (SUP-004/006).
- Third-party nicknames are HTML-escaped by Jinja autoescape (test asserts
  <script> is neutralised).

tests/test_afdian.py +4 (mapping/anonymise, empty, /thanks empty-state,
/thanks list+XSS-escape); test_afdian 17 pass, full suite 129.

Docs: AFD-004 assessing -> done, decisions #3/#4/#6 recorded, Wave 2 all
done, changelog WAVE-20260804-06.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
Surface /thanks from the global left sidebar (heart icon) so the wall is
reachable site-wide, not only from the landing support panel. Verified on
the running dev server with cache-seeded demo sponsors: chips render,
blank name -> "匿名支持者", third-party nickname HTML-escaped.

Docs: AFD-004 evidence updated (sidebar entry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjBDHnJYAW4ETrLWFyj9he
@everbird
everbird merged commit a5e1455 into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant