Skip to content

XP kudos: a live 馃憦 count on each plain-English XP entry - #262

Merged
darkmastermindz merged 2 commits into
mainfrom
xp-kudos
Oct 25, 2026
Merged

darkmastermindz merged 2 commits into
mainfrom
xp-kudos

Conversation

@darkmastermindz

@darkmastermindz darkmastermindz commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

In the About card's plain-English XP view, every entry now has a 馃憦 kudos button with a live count that all visitors share. A Netlify function counts the kudos and keeps them in a separate JSON document in Netlify Blobs, apart from the site's data.

Related Issue

None. Requested directly.

Changes Made

  • netlify/functions/kudos.mjs (/api/kudos):
    • GET returns { counts }. POST { id } adds one kudos and returns { id, count, counted }.
    • The counts are one JSON document (counts) in the site-wide Netlify Blobs store kudos.
    • Only ids of real XP entries count. Stored data is cleaned on every read: no unknown keys, no prototype keys, no non-integers.
    • Once per visitor per entry per day:
      • A marker keyed by an HMAC of IP, entry and day is written with onlyIfNew.
      • The IP address is never stored.
      • The HMAC key comes from the existing sign-in secret, so there's no new setting.
    • Same-origin only: a POST whose Sec-Fetch-Site says another site is refused.
    • Simultaneous kudos: writes to the count are checked against its ETag (onlyIfMatch) and retried, so kudos sent at the same moment are all counted.
  • data/xp.mjs:
    • The XP data (education, experience, achievements) moves out of the About card unchanged, so the card and the function share one list.
    • xpId gives each entry a stable id from the fields that name it, so rewording a line keeps its kudos.
  • composables/useKudos.js:
    • Fetches the counts and refreshes them every 15 seconds while the plain-English XP list is showing and the tab is visible.
    • Counts a tap straight away, and undoes it if the request fails.
    • Remembers in localStorage which entries this browser has already given kudos to.
  • components/AboutCard.vue:
    • A small 馃憦 button on each plain-English XP entry.
    • Its label names the entry and the count, e.g. "Give kudos: Senior Software Engineer at Wells Fargo, 3 so far". The count isn't read out on every refresh.
    • A tap doesn't flip or hold the card.
    • Buttons hide where the function isn't running (a local preview).
  • public/sw.js: the service worker leaves /api/kudos alone, so a count is never an old cached copy.
  • Tests:
    • tests/security/kudos.test.mjs runs against an in-memory store. It covers: once a day per visitor, simultaneous kudos, hostile bodies (KUDOS_BODIES in payloads.mjs) and unknown ids, cross-site requests, methods, and cleaning of stored data.
    • An accessibility test in tests/e2e/a11y.spec.mjs covers the buttons before and after a tap.
  • README: a new paragraph on kudos in "About card".
  • New dependency: @netlify/blobs.

How to Test

  1. Run npm test: 278 tests, including the new kudos tests.
  2. Run npm run generate.
  3. Run PLAYWRIGHT_CHROMIUM_PATH=/opt/pw-browsers/chromium npx playwright test tests/e2e/a11y.spec.mjs -g "About card|XP kudos" --project="Pixel 7" --project="Galaxy Tab S9 landscape". The browser path is only needed here; CI installs its own browser.
  4. On the deploy preview:
    • Open /#xp and hold the card for plain English.
    • Tap 馃憦 on an entry. The count goes up, the button turns green, and the card stays in plain English.
    • Reload: the count stays. A second tap the same day doesn't count again.

Additional Notes

馃 Generated with Claude Code

https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS

- /api/kudos (netlify/functions/kudos.mjs) keeps the counts as one JSON
  document in the "kudos" Netlify Blobs store, separate from site data.
  Known entries only, once per visitor per entry per day (an HMAC of
  IP, entry and day written with onlyIfNew; no IP stored), same-origin
  only, and ETag-checked writes so simultaneous kudos all count
- The XP data moves to data/xp.mjs so the function and the card share
  it; xpId gives each entry a stable id from the fields that name it
- composables/useKudos.js refreshes every 15s while the plain-English
  XP list is showing; buttons hide where the function isn't running
- The service worker leaves /api/kudos alone (never a stale count)
- Security tests with an in-memory store, hostile bodies in
  payloads.mjs, and an accessibility test of the buttons
- The yoga teacher training line says "Graduate" (same as #261)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
# Conflicts:
#	components/AboutCard.vue
@darkmastermindz
darkmastermindz merged commit 05be8c1 into main Oct 25, 2026
23 checks 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.

2 participants