Skip to content

Gear editor (signed in, collected into a PR) + glossary terms with hover definitions - #255

Merged
darkmastermindz merged 9 commits into
mainfrom
gear-editor
Oct 26, 2026
Merged

darkmastermindz merged 9 commits into
mainfrom
gear-editor

Conversation

@darkmastermindz

@darkmastermindz darkmastermindz commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two features.

1. Edit gear lists when signed in. Each gear page gets ✏️ Edit list: change an item's name, brand, quantity, section, category, status, tags or notes, remove it, or add one. Changes show on the page as they'll look. Send for review keeps them on Netlify. The Gear updates GitHub Action then folds every sent edit into one rolling "Gear updates" pull request. It runs weekly, on every push to main and on demand. The same changes on the same list always give the same file, byte for byte.

  • No secrets anywhere:
    • The site signs edits with a key derived from the sign-in secret it already has.
    • The workflow signs in to the site with GitHub Actions' own short-lived token (OIDC).
    • There's no NETLIFY_API_TOKEN, and no Netlify Form.
  • Setup: one GitHub toggle.
  • Diagrams: docs/gear-editing.md (Mermaid).

2. Glossary terms. Acronyms, lingo and jargon in the site's own text get a dotted underline. Hover, focus or tap one for a short definition and a link to its source. Examples: MLH, SPI, AMGA, TIAA, CCDC, CI/CD, RAG, OpenShift, SUP, PFD, Guide Mode.

  • Scope: the About card's plain-English lines, gear tags and notes, and the bio. Blog posts only when they opt in.
  • For future text: a new CLAUDE.md rule and a test make every new piece of display text consider this.

Related Issue

Hansel's requests:

  • "When I'm logged in let me edit/add/remove my gear lists. Quantity, items, tags, etc. this will deterministically create a PR." Plus the follow-ups:
    • one rolling PR;
    • a GitHub Action on a weekly cron and when PRs merge;
    • "It should be able check from the auth" (no separate edit key);
    • "255 shouldn't need NETLIFY_API_TOKEN".
  • "Evaluate and consider adding little backlinks and hover definitions component to acronyms, lingo, and jargon; also add this consideration to every new display text produced."
  • "Draw me a mermaid diagram of this feature and put it in docs folder".

Changes Made

Gear editor

  • Data: the lists move from data/inventory.mjs to data/gear/outdoor.json and camera.json in one canonical form, and each item gets a stable id.
    • data/inventory.mjs reads them; its API and page output are unchanged (checked against a snapshot).
    • A test fails on a hand edit that isn't canonical.
  • utils/gearEdits.mjs: edits are a list of add / update / remove operations.
    • Checked and cut to size: control and bidi characters stripped, limits on everything, only the known fields kept.
    • Applied deterministically: a new item lands after the last one in its category.
    • Serialized canonically.
  • /api/gear/submit (netlify/functions/gear-submit.mjs), for a signed-in owner only, same-origin, POST:
    • checks the changes and signs exactly them: an HMAC over the email, the list and a SHA-256 of the changes (netlify/lib/gearToken.mjs), keyed from GITHUB_CLIENT_SECRET with HKDF and its own label;
    • keeps the signed edit in the Netlify Blobs store gear-edits under pending/<time>-<random>.
  • /api/gear/pending (netlify/functions/gear-pending.mjs): GET lists the pending edits, oldest first, and DELETE { keys } removes the handled ones (only pending/… keys).
    • Who may call it: only a GitHub Actions OIDC token, checked by netlify/lib/githubOidc.mjs:
      • RS256 only, signed with GitHub's published keys (cached for an hour);
      • issuer token.actions.githubusercontent.com;
      • audience hanselwei.dev/gear-updates;
      • repository darkmastermindz/hanselwei.dev, matched by name and by id, along with its owner's id;
      • workflow gear-updates.yml on refs/heads/main;
      • checked for expiry.
  • /api/gear/verify (netlify/functions/gear-verify.mjs) answers only yes or no.
    • Yes means: an edit was signed by a current owner, for exactly this list and these changes, while the signature was valid.
    • Every edit is still checked here before it's applied, so an edit that got into the store any other way is never applied.
  • Workflow (.github/workflows/gear-updates.yml + scripts/gear-updates.mjs):
    • Has id-token: write and asks GitHub for its sign-in token.
    • Reads the pending edits and has the site verify each one. If the site doesn't answer, an edit waits for the next run.
    • Applies the rest oldest first on gear-updates, one commit per edit with a plain-text change list (@mentions and backticks defused).
    • Keeps the branch current with main via a merge, runs npm test, opens or updates the PR, then deletes the handled edits.
    • Runs on push to main instead of pull_request: closed, because a pull request's run is on its merge ref, which the site rightly refuses. Outside Actions it says so and stops.
  • UI: composables/useGearEditor.js, components/GearItemForm.vue and pages/gear/[list].vue.
    • Unsent changes wait in a panel with Undo / Discard all and are kept in the browser.
    • Editing an item that isn't sent yet edits its pending "add".
    • Send for review is one request, to /api/gear/submit.

Glossary

  • data/glossary.js: 34 terms, each with a short definition, a link (official site or Wikipedia) and aliases.
  • utils/glossary.js findTerms: whole-word, case-exact, longest match first, and only the first use per piece of text.
  • Vue components: <Term>, <TermText>, and one <TermTip> in the default layout.
    • Opens on hover, focus (Enter moves into its link) and tap.
    • Escape, a tap elsewhere or a scroll closes it (WCAG 1.4.13).
    • Screen readers get the definition via aria-describedby.
    • Taps on a term inside the About card don't switch the card.
  • server/utils/glossaryHtml.js: marks the bio's markdown after sanitizing.
    • It parses the HTML rather than using regexes, and adds only its own <abbr> + hidden <span>.
    • It skips links, code and headings.
    • Blog posts opt in with glossary: true.
  • CLAUDE.md + tests/app/glossary.test.mjs: new display text considers glossary terms, and the test fails on any undefined acronym in the display text. It reads the XP lines from data/xp.mjs.
  • Plain words: the editor's button says "Send for review" rather than "PR".

Docs:

  • README "Glossary terms" and "Editing gear lists" (setup is now one toggle).
  • docs/gear-editing.md: a Mermaid sequence diagram from Edit list to the merged pull request, and a who-can-call-what diagram.
  • CONTENT-NOTICE / README license note now name data/gear/.

Merged with main (now through #266):

  • The plain-English XP line keeps both the glossary terms (<TermText>) and the 👏 kudos button.
  • payloads.mjs keeps GEAR_TEXT, KUDOS_BODIES, BAD_BEARER_TOKENS and BAD_COMMITS.

How to Test

  1. npm test: 333 pass.
    • tests/security/github-oidc.test.mjs: a genuine token for this workflow on main is accepted. Each of these is refused:
      • no signature (alg: none), or HS256 "signed" with GitHub's public key;
      • a stranger's key, a tampered payload, or an unknown key id;
      • a wrong issuer, audience, repository, repository id or owner id;
      • another workflow, a branch or pull-request ref;
      • an expired or not-yet-valid token, and malformed tokens (BAD_BEARER_TOKENS in payloads.mjs).
        It also covers GitHub's keys being cached and refetched at most once a minute, and that /api/gear/pending only reads and deletes pending/ keys, for that token only.
    • tests/security/gear-edits.test.mjs:
      • submit keeps edits only for a signed-in owner from this site, and nothing for bad lists or changes;
      • what it keeps is signed for exactly the checked changes, and /api/gear/verify agrees;
      • tests for hostile text and for forging, expiring or tampering with edit signatures.
    • tests/app/gear-edits.test.mjs: determinism, ordering, and submissions through the real verify handler.
  2. npm run generate builds. The About card, kudos and gear accessibility tests pass on a phone and a tablet (10 passed).
  3. Glossary:
    • On /bio, hover "UNC Charlotte" or "ARC".
    • On the home page, open XP, hold for plain English, and tap "MLH" or "TIAA". The card doesn't switch.
  4. Done locally:
    • Workflow dry run: scripts/gear-updates.mjs ran against stand-in GitHub (OIDC token and API) and site servers using the real submit, pending and verify handlers.
      • The owner's edit became one commit and the "Gear updates" PR, and a forged edit was dropped.
      • Both were deleted from the store, a second run found nothing, and outside Actions it stops with a message.
    • Editor in Chromium with mocked sign-in: Send for review makes one POST /api/gear/submit with the changes, shows "Sent", and passes axe.
    • Both diagrams render with Mermaid 11.

Additional Notes

One-time setup after merging: GitHub → Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests". There are no secrets to add and nothing to turn on at Netlify.

Limits:

  • Changing GITHUB_CLIENT_SECRET signs everyone out, and voids edits sent but not yet applied (send them again). It also restarts the daily kudos limit.
  • A PR opened with the workflow's own token doesn't start other workflows, so the script runs the tests itself; the Netlify deploy preview still builds.
  • The editor shows the live lists plus your unsent changes, not edits already waiting in the Gear updates PR.
  • New lists are added by hand.
  • Deploy previews share the gear-edits store with the live site (it's site-wide). Only owners can add to it.

Other: the glossary modules are .js because Nitro left .mjs imports from server/utils unbundled with a path that broke the prerender.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS

Comment thread tests/security/glossary.test.mjs Fixed
… source

Acronyms, lingo and jargon in the site's own text (MLH, SPI, TIAA, CI/CD,
SUP, Guide Mode, ...) get a dotted underline and a definition bubble with a
link to where the term comes from.

- data/glossary.js: each term's short definition, link and other spellings
- <Term> and <TermText> mark Vue text (first use per piece of text);
  glossaryHtml marks the bio's rendered markdown, and blog posts that opt in
  with `glossary: true`, after sanitizing (parsed, not regex-edited)
- <TermTip> in the layout: opens on hover, focus (Enter moves to its link)
  and tap; Escape, a tap elsewhere or scrolling closes it (WCAG 1.4.13).
  Screen readers get the definition as the term's description. Taps on a
  term inside the About card don't switch the card.
- The About card's plain-English lines use it
- CLAUDE.md: consider this for all new display text; a test fails on an
  acronym in the display text that has no definition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
… a PR

Signed in, each gear page has "Edit list": change an item's name, brand,
quantity, section, category, status, tags or notes, remove it, or add one.
Changes preview on the page and wait in the browser until sent.

- The lists move to data/gear/*.json in one canonical form (same output on
  the site); utils/gearEdits.mjs checks and applies edits the same way
  everywhere, so the same changes always give the same file
- Send: /api/gear/token signs exactly those changes for a signed-in owner
  (HMAC with GEAR_EDIT_KEY, 15 minutes), then they go to the "gear-edit"
  Netlify Form
- The Gear updates workflow (weekly, on every merge to main, on demand)
  reads the submissions, drops any whose signature doesn't check out, applies
  the rest in order on the gear-updates branch (one commit each), runs the
  tests, opens or updates the one "Gear updates" pull request and deletes
  the submissions it handled
- Gear tags and notes show glossary terms (SUP, PFD, Guide Mode, ...)
- README "Editing gear lists" has the one-time setup; until it's done the
  workflow logs "not set up" and passes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
The key that signs gear edits now comes from GITHUB_CLIENT_SECRET (HKDF with
its own label, like the session key), so GEAR_EDIT_KEY is gone. The Gear
updates workflow no longer holds any key: it asks the site to check each
submission (POST /api/gear/verify, yes or no only), and a submission the
site can't be asked about waits for the next run instead of being dropped.

Setup is down to one GitHub secret, NETLIFY_API_TOKEN, to read and delete
the form's submissions; the site ID is in the workflow (it isn't secret).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
The XP list line keeps both the glossary terms (TermText) and the new
kudos button; payloads keep both GEAR_TEXT and KUDOS_BODIES. The XP text
moved to data/xp.mjs, so the glossary test reads it from there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
The Netlify connection only lets Netlify read the repo, so the workflow
needed NETLIFY_API_TOKEN to read and delete Netlify Form submissions.
Now nothing is stored anywhere:

- /api/gear/submit (was /api/gear/token): for a signed-in owner only,
  checks and signs the changes and keeps them in the "gear-edits"
  Netlify Blobs store under pending/<time>-<random>. The Netlify Form
  (public/forms/gear-edit.html) is gone.
- /api/gear/pending: GET the pending edits, DELETE the handled ones,
  only for a GitHub Actions OIDC token signed by GitHub (RS256 against
  its published keys) for this repository's gear-updates.yml on main
  (netlify/lib/githubOidc.mjs).
- The workflow asks GitHub for that token (id-token: write), runs on
  push to main instead of pull_request closed (a pull request's run is
  on its merge ref), and still has each edit verified before applying.
- Tests: forged, expired, wrong-audience/repo/workflow/branch tokens,
  alg none and HS256 confusion, key caching; pending reads and deletes
  only pending/ keys; submit keeps exactly what it signed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
docs/gear-editing.md: a sequence diagram from Edit list to the merged
Gear updates pull request (submit, Blobs, the workflow's GitHub OIDC
sign-in, pending, verify, apply, delete), and who can call what.
Linked from the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
The workflow's GitHub token must also carry this repository's id and
its owner's id, which stay the same through a rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nginuw5wmUjUSkmQB1J3pS
# Conflicts:
#	tests/security/payloads.mjs
@darkmastermindz
darkmastermindz merged commit 42af6d5 into main Oct 26, 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.

3 participants