Skip to content

Add gamification badges - #3

Open
OSPFNeighbour wants to merge 1 commit into
mainfrom
pr/gamification
Open

Add gamification badges#3
OSPFNeighbour wants to merge 1 commit into
mainfrom
pr/gamification

Conversation

@OSPFNeighbour

@OSPFNeighbour OSPFNeighbour commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

PersonBadge/PersonBadgeProgress ids were just the badge-type id (e.g.
first-signin-location-), which is shared by every member
who first signed in at that location. Since Relay normalizes its store
by raw id, responses for different members collided and silently
clobbered each other's awardedAt (and, once badgeId was split out,
PersonBadge/PersonBadgeProgress collided with each other too). Scope
ids per-person and per-type ("PersonBadge::"), and
expose the badge-type id separately as badgeId for icon/type lookups.

Also includes the in-progress badge catalog/icon work: a new April
Fools' Day badge, badge digest/export fixes, and new SVG badge assets.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Members should choose which of their own locations get a weekly badge
digest email, matching the existing daily email summary pattern,
instead of an admin-wide location toggle sending to everyone with
access.

Exposes current/target counts for badges backed by a running counter
(sign-ins, category thresholds, streaks), computed from the per-location
counters already stored on the Person record, so no extra DB reads are
needed. Manual (nightly-batch/easter-egg) and fixed-date badges keep the
plain earned/locked state since they have no meaningful running total.
Renders as a compact progress bar in the shared badge panel used by the
admin member page, the badges page, and the kiosk modal.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Previously the "new badge" prompt was shown whenever the member's home
location had gamification enabled, regardless of whether the location
they were currently scanning at had it disabled. Now the celebration
is only shown when the current location has gamification enabled;
pending awards from any location are left undisplayed (badges are
global to the person, not scoped to a location) so they surface next
time the member is somewhere gamification is on, deduped so a badge
independently re-triggered at a second location doesn't double-pop.

Also fixes a passport (first-signin-at-location) badge showing the
wrong location name/description once it could surface at a location
other than where it was earned.

Give the location "first sign-in" badge icon actual stamp iconography
(worn double ring, star emblem, ribboned unit text, tilted impression)
instead of plain concentric circles with a stray unused ring.

Replace the two crossed lines and dot placeholder with a recognizable
wrench glyph.

Replace the plain filled circle placeholder with a folded map, dotted
trail, and X marking the spot.

Extract the tier key/CSS-class/label helpers out of
MemberBadgeProgressPanel into a shared lib/badgeTiers module, and use
them in the members list to replace the single badge-count-plus-last-
earned cell with a breakdown of counts per tier (gold/silver/bronze/
starter/passport stamps).

Add a tierCounts field to the badgeLeaderboard GraphQL entry, grouping
each person's awarded badges by tier. Use it in the kiosk leaderboard
to show a per-tier pill breakdown on each row and an aggregate
breakdown in the summary bar, instead of a single opaque total.

createPeriod bypassed badge progress entirely since only scan_register2/
scan_sign_out called apply_badge_event. Now createPeriod feeds a synthetic
check-in/sign-out pair through the same counters/streaks/easter-egg logic,
using the period's own start/end time rather than wall-clock time (so
backdated entries evaluate holiday/streak rules against when the period
actually happened). Any award earned this way is left undisplayed so it
surfaces on the member's next real kiosk visit rather than being silently
consumed with no UI to show it.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

The gamification branch predated the full CSS->Tailwind conversion on
main and kept adding to style.css/kiosk.css/global.css. Those files
were resurrected during the rebase so history would apply cleanly;
this converts their remaining rules (badge cards, progress bars, kiosk
leaderboard/member-badges modals, celebration overlay, tier pills) to
Tailwind utility classes and deletes the legacy stylesheets.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

  • Add bottom margin below the tier-count pill row on the kiosk leaderboard
    so it doesn't butt up against the member list.
  • Explicitly center tier/source group headers in MemberBadgeProgressPanel
    instead of relying on inherited page text-align, so kiosk and admin render
    it the same way.
  • Restore pointer-events: none on the badge celebration confetti layer,
    lost when its CSS was converted to Tailwind, which was silently blocking
    clicks on the backdrop meant to dismiss the overlay.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

@sdunster

sdunster commented Jul 2, 2026

Copy link
Copy Markdown
Member

Rebased onto fresh main, squashed minor fixes, force-pushed.

@OSPFNeighbour

Copy link
Copy Markdown
Collaborator Author

Added two more dashboard style interfaces on the kiosk screen - location leaderboard and member badges - both are only loaded on demand so shouldnt hurt performance unless clicked.

UI on that kiosk is hard to work with, really need to do a CSS redo on it

@OSPFNeighbour

Copy link
Copy Markdown
Collaborator Author

ive just ran a bunch of tests of the digest emails and had to make some web resource changes to support making that email html not look like total crap.

@OSPFNeighbour

Copy link
Copy Markdown
Collaborator Author

@sdunster logic I went with is if a members home location has gamification enabled members will earn badges but if the location they are signing in at doesn't have them enabled they won't be displayed, next sign in at an enabled location they will get the popup and have them awarded. This logic extends to badges awarded from API calls and overnight code runs (they get to see them next sign in)

@OSPFNeighbour OSPFNeighbour changed the title WIP - Badge Gamification Badge Gamification Jul 7, 2026

@sdunster sdunster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will need rebasing

@OSPFNeighbour

Copy link
Copy Markdown
Collaborator Author

@sdunster a question for you - the email it sends links back to the svg's in the web - how do you want to handle url's for these with prod/dev etc

@OSPFNeighbour
OSPFNeighbour requested a review from sdunster July 10, 2026 09:55
@OSPFNeighbour OSPFNeighbour added the enhancement New feature or request label Jul 16, 2026

@sdunster sdunster left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extremely light review - many parts not yet covered - please dont act on it yet

random thoughts:

  • these icons may go further if you extracted just the icon from the background and overlayed them on either an SVG or just a html/css circle background. This way you could have bronze, silver, gold, platinum variants all using the same SVG "icon"/"foreground"
  • for rollout I reckon we should potentially even go further and make the logic such that we start by allowlisting gamification to individual people
  • perhaps it'd make sense to have 2 different axes for badge grouping.. one is the "tier"/"class" ie bronze, silver, gold and the other is the "group"/"type"/"category" ie your passport ones vs streaks vs .. more of like a functional thing rather than how significant the achievement is?

Ok(Some(id)) => id,
Ok(None) => return true,
Ok(None) => {
info!("request_auth_code: no user found for email={}", email);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split

end_time: i64,
category_id: ID,
) -> Result<Period<A>> {
) -> Result<ScanSignOutResult<A>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally get why this changed but can't just do it this way. Gotta make a new "scan_sign_out2" variant so we don't break existing callers during rollout. Most of the time the window is small but kiosks sometimes take a while to get the new code. Best way to validate this (CI will flag it for you) is to split backend from frontend changes into separate PRs so you simulate them releasing at different times.

Comment thread api/src/badge_digest.rs
use crate::db;
use crate::mail;

const DEFAULT_BADGE_ICON_BASE_URL: &str = "https://new.seslogin.com/image/badges";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine for now. The links will be broken until the images make it to prod. If you'd like for that not to be the case you could split them out to a separate PR and land that first.

Comment thread api/src/db.rs
email: Option<&'a str>,
},
BadgeState {
badge_state: serde_json::Map<String, serde_json::Value>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdunster: think about this some more - should we prefer richer types or is this just passthrough?

Comment thread api/src/dynamodb.rs
(None, Some(msg)) => msg.to_string(),
(None, None) => format!("{e}"),
// For non-service failures (dispatch/timeout), Debug includes connector cause.
(None, None) => format!("{e:?}"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split

Comment thread infra/iam_users.tf
# CI/CD authenticates via GitHub OIDC — the OIDC provider, deploy role, and its
# scoped policies (Lambda code update, S3 web sync, CloudFront invalidation) are
# defined in iam_oidc.tf (added in Phase 5).
## Human/admin access is via IAM Identity Center (SSO) — see the SesloginAdmin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert?

});
}

function formatRelativeHours(tsSeconds: number): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to time.ts or use an existing function from there

return `inline-flex min-w-5.5 items-center justify-center rounded-full border px-2 py-0.75 text-[0.85rem] font-extrabold ${TIER_PILL_CLASS[tierCssClass(tier)]}`;
}

function LeaderboardModalLoadingFallback(props: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these functional components all really should be separate files

<ScanTitleBar
onCancelSignOut={cancelSignOut ?? undefined}
signingOutName={signingOutName ?? undefined}
signingOutPersonId={signingOutPersonId ?? undefined}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sdunster: investigate

Comment thread .gitignore
infra/terraform.tfvars
infra/.terraform/
infra/.terraform.lock.hcl
.vscode/settings.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split

@OSPFNeighbour OSPFNeighbour changed the title Badge Gamification Add gamification badges Jul 21, 2026
PersonBadge/PersonBadgeProgress ids were just the badge-type id (e.g.
first-signin-location-<locationId>), which is shared by every member
who first signed in at that location. Since Relay normalizes its store
by raw id, responses for different members collided and silently
clobbered each other's awardedAt (and, once badgeId was split out,
PersonBadge/PersonBadgeProgress collided with each other too). Scope
ids per-person and per-type ("PersonBadge:<personId>:<badgeId>"), and
expose the badge-type id separately as badgeId for icon/type lookups.

Also includes the in-progress badge catalog/icon work: a new April
Fools' Day badge, badge digest/export fixes, and new SVG badge assets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>




Members should choose which of their own locations get a weekly badge
digest email, matching the existing daily email summary pattern,
instead of an admin-wide location toggle sending to everyone with
access.




Exposes current/target counts for badges backed by a running counter
(sign-ins, category thresholds, streaks), computed from the per-location
counters already stored on the Person record, so no extra DB reads are
needed. Manual (nightly-batch/easter-egg) and fixed-date badges keep the
plain earned/locked state since they have no meaningful running total.
Renders as a compact progress bar in the shared badge panel used by the
admin member page, the badges page, and the kiosk modal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>




Previously the "new badge" prompt was shown whenever the member's home
location had gamification enabled, regardless of whether the location
they were currently scanning at had it disabled. Now the celebration
is only shown when the current location has gamification enabled;
pending awards from any location are left undisplayed (badges are
global to the person, not scoped to a location) so they surface next
time the member is somewhere gamification is on, deduped so a badge
independently re-triggered at a second location doesn't double-pop.

Also fixes a passport (first-signin-at-location) badge showing the
wrong location name/description once it could surface at a location
other than where it was earned.






Give the location "first sign-in" badge icon actual stamp iconography
(worn double ring, star emblem, ribboned unit text, tilted impression)
instead of plain concentric circles with a stray unused ring.




Replace the two crossed lines and dot placeholder with a recognizable
wrench glyph.




Replace the plain filled circle placeholder with a folded map, dotted
trail, and X marking the spot.




Extract the tier key/CSS-class/label helpers out of
MemberBadgeProgressPanel into a shared lib/badgeTiers module, and use
them in the members list to replace the single badge-count-plus-last-
earned cell with a breakdown of counts per tier (gold/silver/bronze/
starter/passport stamps).




Add a tierCounts field to the badgeLeaderboard GraphQL entry, grouping
each person's awarded badges by tier. Use it in the kiosk leaderboard
to show a per-tier pill breakdown on each row and an aggregate
breakdown in the summary bar, instead of a single opaque total.




createPeriod bypassed badge progress entirely since only scan_register2/
scan_sign_out called apply_badge_event. Now createPeriod feeds a synthetic
check-in/sign-out pair through the same counters/streaks/easter-egg logic,
using the period's own start/end time rather than wall-clock time (so
backdated entries evaluate holiday/streak rules against when the period
actually happened). Any award earned this way is left undisplayed so it
surfaces on the member's next real kiosk visit rather than being silently
consumed with no UI to show it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>




The gamification branch predated the full CSS->Tailwind conversion on
main and kept adding to style.css/kiosk.css/global.css. Those files
were resurrected during the rebase so history would apply cleanly;
this converts their remaining rules (badge cards, progress bars, kiosk
leaderboard/member-badges modals, celebration overlay, tier pills) to
Tailwind utility classes and deletes the legacy stylesheets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>




- Add bottom margin below the tier-count pill row on the kiosk leaderboard
  so it doesn't butt up against the member list.
- Explicitly center tier/source group headers in MemberBadgeProgressPanel
  instead of relying on inherited page text-align, so kiosk and admin render
  it the same way.
- Restore pointer-events: none on the badge celebration confetti layer,
  lost when its CSS was converted to Tailwind, which was silently blocking
  clicks on the backdrop meant to dismiss the overlay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants