Part 7 · Gamifying Learning: 25 Badges, Idle-Gap Sessions, and a 90-Day Heatmap

May 20, 2026·
Ndimofor Aretas
Ndimofor Aretas
· 7 min read
blog Product

Part of a series on building Gemma CogniVault. Previously: The mindmap renderer — hand-rolled SVG to React Flow.

All abbreviations are fully explained in the appendix at the bottom of the page.

I taught secondary-school ICT for eight years before pivoting to full-stack development, and the most reliable lesson from that period was uncomfortably simple: students who showed up consistently learned. Students who didn’t, didn’t. Talent, prior knowledge, even motivation on any given day — all of it was downstream of attendance.

CogniVault’s “Dashboard” tab is a small attempt to engineer for that. It’s not a Duolingo streak panic machine. It’s three things:

  • Hero stats — total study time, total sessions, current streak.
  • 25 achievement badges — auto-tracked across chat, quizzes, workshops, flashcards, mindmaps.
  • A 90-day activity heatmap — GitHub-style, five purple intensity levels.

The whole thing is a small SQLite table-set and a few React components. The interesting part isn’t the code, though — it’s the design calls.

Idle-gap sessions

The hardest question turned out to be the simplest-sounding: what counts as one study session?

The naive answer is “anything bookended by open/close of the app.” That’s wrong. People leave tabs open. People bounce away for an hour and come back. People open the app at 9am, do nothing, and check in at 2pm.

The answer I landed on: a session ends when you’ve been idle for 15 minutes. Ask a question, idle for 16 minutes — that’s one session. Come back, ask another — that starts a new one. The threshold is configurable via STUDY_SESSION_IDLE_GAP_SECONDS=900.

The clock keys off chat messages — the conversational core of studying in CogniVault. Every message either extends the open session (bumping its ended_at timestamp and message count) or, if the gap since the last activity exceeds the threshold, closes it implicitly and opens a new one:

# Simplified from backend/services/progress_tracker.py
def record_message(now: float, idle_gap: int):
    last = most_recent_session()
    if last and (now - last.ended_at) <= idle_gap:
        extend(last, ended_at=now)          # same session continues
    else:
        open_session(started_at=now, ended_at=now)   # new session begins

Two writes per message. A session’s duration is ended_at - started_at, which means “total time” reflects engaged time, not “had a tab open.” Which is the only number that actually means anything. (Study Hub actions — quiz attempts, card flips, mindmap exports — are recorded as their own events and feed the badge metrics below; the session clock itself stays message-driven and honest.)

25 badges, not 250

Most gamified apps absolutely flood you with achievements. There’s a reason: more badges, more dopamine, more daily active users. The cost is that each badge means less — eventually the whole layer becomes wallpaper.

I capped CogniVault at 25, split across the five activity surfaces:

  • 10 for chat & study habits (first question, 10 messages in a day, 100 total, an hour of total study, 3- and 7-day streaks, a 30-minute deep-dive session, night-owl and early-bird sessions, first use of the scope filter)
  • 4 for quizzes (first quiz, a perfect score, passing on advanced difficulty, 10 quizzes)
  • 4 for workshops (first outline, first completed lesson, first completed workshop, 5 completed)
  • 4 for flashcards (first deck, 50 card flips, fully mastering a deck, 5 decks)
  • 3 for mindmaps (first mindmap, first export, 5 mindmaps)

Each badge has a one-line unlock criterion that’s auto-evaluated on relevant events. Nothing manual, nothing the user has to “claim.” They just appear.

And the definitions aren’t code at all — they’re data. All 25 live in a single JSON file, each entry naming the metric it watches and the target to hit:

{
  "code": "card_reviewer",
  "name": "Card Reviewer",
  "icon": "🃏",
  "metric": "total_card_flips",
  "target": 50
}

A single evaluator reads the current stats, compares every definition against its metric, diffs against already-earned badges, and inserts new unlocks into progress.db. Adding badge number 26 means adding a JSON entry, not writing new logic. Several badges form ladders — each knows which badge is its “next level,” which powers the detail view’s nudge toward the next goal.

The heatmap

The 90-day heatmap is the part I’m proudest of, and also the simplest. It’s a 13×7 grid of cells, one per day, coloured by total study time that day.

Five intensity levels:

level 0 — no activity
level 1 — under 15 minutes      (a quick check-in)
level 2 — 15-60 minutes         (a focused session)
level 3 — 1-3 hours             (substantial study)
level 4 — 3+ hours              (a marathon)

The data is conceptually one aggregation over the sessions table:

SELECT date(started_at) AS day,
       SUM(ended_at - started_at) AS seconds
FROM study_sessions
WHERE started_at >= date('now', '-90 days')
GROUP BY day;

The backend zero-fills the missing days so the frontend always receives exactly 90 entries, and a small client-side function bins each day’s total into the five levels. Click any cell and a DayDetailModal opens with that day’s numbers — study time, sessions, messages — plus any badges earned that day.

The reason I love this component: it makes the texture of a study habit visible. Streaks are great, but a streak is one number. A heatmap shows you that you study harder on weekends, or that you’ve been on a slow drift downward all month, or that the gap between your last “level 4 day” and today is longer than you thought. It reflects something the user can act on.

What I deliberately left out

Three things you’d find in most gamified apps, intentionally absent from CogniVault:

  1. Streak panic. No “your streak is in danger!” pop-up. No streak freeze rules. No yellow exclamation marks. The streak is shown — that’s the entirety of the feedback loop. If a user breaks their streak, they break their streak. Adults don’t need shaming UX.

  2. Leaderboards. This is a single-user, fully local app. There’s no global comparison. (And there shouldn’t be — leaderboards optimise the wrong thing for studying.)

  3. Confetti, fanfare, push notifications. A newly earned badge shows up in the quiz results screen and on the dashboard grid. That’s the entire celebration. Anything bigger is theft of the user’s attention for the app’s benefit, not theirs.

The general principle: measure what matters, surface it without nagging. Notice that you came back. Reflect that back to you. Don’t pretend you care more than you do.

What the dashboard doesn’t try to optimise

A common trap with these dashboards is reverse-causation: the user starts gaming the metric instead of doing the underlying thing. A daily-question count, for instance, gets you users who ask one filler question per day to keep their streak alive.

So the bar is deliberately low in exactly one place and high everywhere else. There is one zero-effort badge — “First Question,” earned on your very first message — because every game needs an on-ramp that proves the system works. After that, the metrics get hard to game without doing the actual work:

  • Total study time — only accrues during active engagement, with idle-gap cutoffs.
  • Sessions — adding more requires actually starting separate work periods.
  • Badges — almost all require depth (100 messages, ace a quiz, master a deck, complete 5 workshops), not just touch.
  • Heatmap intensity — needs sustained engagement on a given day.

Implementation: small on purpose

The gamification core is three SQLite tables —

study_sessions       (id, started_at, ended_at, message_count)
message_events       (id, sent_at, session_id, had_scope_filter, had_attachments)
achievements_earned  (code, earned_at)

— plus the JSON badge definitions, one evaluator module, and a handful of React components (SummaryCards, AchievementGrid, ActivityHeatmap, DayDetailModal). The same progress.db file has since grown more tables for the Study Hub’s saved quizzes, workshops, decks, and mindmaps — but the badge-and-session machinery itself remains a couple hundred lines.

There’s nothing fancy in any of it. The dashboard works because the design calls are right, not because the implementation is clever.

Takeaway

If you’re building a learning tool — or any tool that lives on user habit — gamify consciously. Pick the metrics that reflect what you actually want to encourage. Cap your achievement surface. Skip the streak-panic UX. Make the texture of usage visible without making the app desperate.

Or, more bluntly: don’t build Duolingo. Build a dashboard the user occasionally glances at and then closes, feeling slightly more inclined to keep going. That’s the whole job.


Appendix: Abbreviations in this post

AbbreviationFull formMeaning
UXUser ExperienceHow the product feels — the thing streak-panic mechanics sacrifice
ICTInformation and Communications TechnologyThe subject I taught for eight years before going full-stack
SQLite(SQL = Structured Query Language)A complete relational database living in one file, progress.db
JSONJavaScript Object NotationThe data format the 25 badge definitions live in
UIUser InterfaceThe dashboard surface: stats, grid, heatmap

Next up: Testing a local-AI app — 351 tests, mocked Ollama, zero infrastructure.

Ndimofor Aretas
Authors
IT Trainer & Fullstack Developer
An experienced developer and certified IT trainer (IHK) based in Germany, passionate about sharing knowledge and making complex technical concepts accessible through hands-on technical content creation and projects.