<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gamification |</title><link>https://aretascodes.dev/tags/gamification/</link><atom:link href="https://aretascodes.dev/tags/gamification/index.xml" rel="self" type="application/rss+xml"/><description>Gamification</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://aretascodes.dev/media/icon_hu_2ab4f4763b27c75b.png</url><title>Gamification</title><link>https://aretascodes.dev/tags/gamification/</link></image><item><title>CogniVault Backend Explained, Part 4 · Study Tools, Progress, and the Privacy Receipts</title><link>https://aretascodes.dev/blog/backend-explained-study-hub-privacy/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/blog/backend-explained-study-hub-privacy/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;All abbreviations are fully explained in the appendix at the bottom of the page.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In
we followed a question through hybrid retrieval and the agent loop to a cited answer. In this final part, the same machinery gets pointed at a different goal: &lt;em&gt;teaching you&lt;/em&gt; — and then we close the series by auditing the project&amp;rsquo;s central promise: nothing leaves your machine.&lt;/p&gt;
&lt;h2 id="one-recipe-four-study-tools"&gt;One recipe, four study tools&lt;/h2&gt;
&lt;p&gt;CogniVault generates quizzes, multi-lesson workshops, flashcard decks, and mindmaps from your documents. Four different outputs — but under the hood, one shared five-step recipe:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retrieve.&lt;/strong&gt; The same hybrid search from Part 3, but instead of your question, the probe is a broad query like &lt;em&gt;&amp;ldquo;key concepts, definitions, important facts, main ideas&amp;rdquo;&lt;/em&gt;, scoped to the documents you selected. Up to 15 representative chunks come back.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prompt from a template.&lt;/strong&gt; The instructions sent to Gemma are not buried in Python — they&amp;rsquo;re editable Markdown files in &lt;code&gt;backend/prompts/&lt;/code&gt; (&lt;code&gt;quiz.md&lt;/code&gt;, &lt;code&gt;flashcards.md&lt;/code&gt;, and so on). Drop a modified copy into &lt;code&gt;backend/prompts/custom/&lt;/code&gt; and it overrides the shipped version on the very next request. No restart, no code change. Prompt engineering as configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Constrain the output.&lt;/strong&gt; Asking a small local model to &amp;ldquo;please return JSON&amp;rdquo; works most of the time — and &lt;em&gt;most of the time&lt;/em&gt; is a production bug. CogniVault uses Ollama&amp;rsquo;s grammar-constrained generation (&lt;code&gt;format=&amp;quot;json&amp;quot;&lt;/code&gt;), which makes invalid JSON impossible rather than unlikely, plus low temperature for consistency. The full saga of getting reliable structure out of a 4-billion-parameter model is in
.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Validate defensively.&lt;/strong&gt; Every generated item is checked field by field, and malformed items are &lt;em&gt;dropped&lt;/em&gt; rather than failing the whole batch. Small models occasionally fumble one question out of ten; a product shouldn&amp;rsquo;t collapse because of it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Persist.&lt;/strong&gt; Everything lands in SQLite, so quizzes are resumable, workshop progress survives restarts, and flashcard statuses are remembered per deck.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;rsquo;s the recipe in motion for a quiz:&lt;/p&gt;
&lt;div class="mermaid"&gt;%%{init: {'sequence': {'actorFontSize': 28, 'messageFontSize': 24, 'loopTextFontSize': 22, 'noteFontSize': 22}}}%%
sequenceDiagram
actor U as You
participant F as Study Hub UI
participant B as FastAPI
participant V as VectorDB
participant O as Ollama (gemma4:e4b)
participant S as SQLite
U-&gt;&gt;F: Pick scope, difficulty, question count
F-&gt;&gt;B: POST /api/study/quiz/generate
B-&gt;&gt;V: Hybrid search, scoped to your documents
V--&gt;&gt;B: Up to 15 representative chunks
B-&gt;&gt;B: Render the quiz.md prompt template
B-&gt;&gt;O: chat(format="json", low temperature)
O--&gt;&gt;B: Grammar-constrained JSON
B-&gt;&gt;B: Validate each question, drop bad ones
B-&gt;&gt;S: Save quiz (resumable later)
B--&gt;&gt;F: Typed response
F--&gt;&gt;U: Play, submit, score — and maybe a new badge
&lt;/div&gt;
&lt;p&gt;The four tools differ only in their template and their shape: quizzes produce multiple-choice and true/false questions with explanations; workshops produce an outline first and then write each lesson &lt;em&gt;on demand&lt;/em&gt; when you open it; flashcards produce front/back pairs; mindmaps produce a topic tree that the frontend renders as an interactive diagram. (That renderer is its own adventure:
.)&lt;/p&gt;
&lt;h2 id="sessions-that-track-themselves"&gt;Sessions that track themselves&lt;/h2&gt;
&lt;p&gt;Most study apps make you press a start button, and most people forget. CogniVault takes a different stance: &lt;strong&gt;study sessions are inferred, not declared&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Every chat message either extends the current session or — after a 15-minute idle gap — quietly starts a new one. Walk away for coffee, come back, keep working: same session. Come back tomorrow: new session. No buttons, no forgetting.&lt;/p&gt;
&lt;p&gt;Each message also records a tiny event (timestamp, whether you used a scope filter or attachments) into &lt;code&gt;progress.db&lt;/code&gt; — a SQLite database, which is a complete relational database living in a single file. Eleven tables hold everything: sessions, message events, earned badges, quiz attempts and saved quizzes, workshops and lessons, decks and cards, and mindmaps.&lt;/p&gt;
&lt;p&gt;One engineering note worth copying: the tracking call inside the chat endpoint is wrapped so that it can &lt;em&gt;never&lt;/em&gt; block or break the chat. Analytics must be a passenger, never a driver.&lt;/p&gt;
&lt;h2 id="25-badges-defined-as-data"&gt;25 badges, defined as data&lt;/h2&gt;
&lt;p&gt;The achievements aren&amp;rsquo;t scattered through the code as &lt;code&gt;if&lt;/code&gt; statements. They live in one JSON file — 25 entries, each with a code, a name, an icon, the metric it watches, and a target. After each relevant action, an evaluator checks every definition against the database and persists anything newly earned. Some badges form ladders, each pointing to its next level.&lt;/p&gt;
&lt;p&gt;Declarative beats imperative here for a simple reason: adding badge number 26 means adding a JSON entry, not writing new logic. The design behind the streaks, the idle-gap rule, and the 90-day heatmap got its own post:
.&lt;/p&gt;
&lt;h2 id="voice-input-without-a-cloud-microphone"&gt;Voice input, without a cloud microphone&lt;/h2&gt;
&lt;p&gt;The microphone button is powered by &lt;strong&gt;faster-whisper&lt;/strong&gt; — OpenAI&amp;rsquo;s Whisper speech-recognition model re-implemented on a faster inference engine — running on your CPU with int8 quantisation (8-bit numbers instead of 32-bit: smaller, faster, accurate enough). No audio ever leaves the machine.&lt;/p&gt;
&lt;p&gt;The model is lazy-loaded on the first transcription so app startup stays instant, and if faster-whisper isn&amp;rsquo;t installed at all, the frontend simply hides the mic button. Features should degrade, not detonate.&lt;/p&gt;
&lt;h2 id="the-privacy-receipts"&gt;The privacy receipts&lt;/h2&gt;
&lt;p&gt;The series began with a promise: &lt;em&gt;nothing leaves your machine.&lt;/em&gt; Promises are cheap — here&amp;rsquo;s the audit. Every byte CogniVault stores, and where it lives:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your uploaded files&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docs/&lt;/code&gt; folder&lt;/td&gt;
&lt;td&gt;The original files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search vectors&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vector_store.faiss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FAISS binary index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chunk text and metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vector_store.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-to-category map&lt;/td&gt;
&lt;td&gt;&lt;code&gt;categories.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat sessions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chat_history.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions, badges, quizzes, workshops, decks, mindmaps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;progress.db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingestion checkpoints&lt;/td&gt;
&lt;td&gt;PostgreSQL (local Docker volume)&lt;/td&gt;
&lt;td&gt;DBOS system tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The AI models themselves&lt;/td&gt;
&lt;td&gt;Ollama&amp;rsquo;s local model store&lt;/td&gt;
&lt;td&gt;Model weights&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Nothing in that table is on someone else&amp;rsquo;s computer. Inference goes to &lt;code&gt;localhost&lt;/code&gt;. Embeddings go to &lt;code&gt;localhost&lt;/code&gt;. The only outbound request the backend ever makes is the URL-import feature — at your explicit request, and guarded against fetching private addresses. The app even surfaces these stats live in its Privacy Vault Audit panel.&lt;/p&gt;
&lt;p&gt;And because trust needs more than a table: the whole backend is covered by a pytest suite you can run yourself — the approach is documented in
.&lt;/p&gt;
&lt;h2 id="series-wrap-up"&gt;Series wrap-up&lt;/h2&gt;
&lt;p&gt;Four parts, one architecture:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;
&lt;/strong&gt; — three processes, four layers, and a decoder ring for the jargon&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;
&lt;/strong&gt; — a durable, format-aware pipeline that turns any document into searchable vectors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;
&lt;/strong&gt; — two retrievers covering each other&amp;rsquo;s blind spots, fused by rank, driven by an agent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4&lt;/strong&gt; — the same machinery generating study materials, tracking progress without buttons, and a storage map with no cloud rows in it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If there&amp;rsquo;s one theme, it&amp;rsquo;s this: &lt;strong&gt;boring, verifiable choices in service of privacy&lt;/strong&gt;. Exact search instead of approximate. SQLite files instead of hosted databases. Grammar-constrained JSON instead of hopeful parsing. Soft deletes instead of clever index surgery. Every piece is something you can open, read, and check — which is exactly the point.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="appendix-abbreviations-in-this-post"&gt;Appendix: Abbreviations in this post&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abbreviation&lt;/th&gt;
&lt;th&gt;Full form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;The structured format the generators force the model to produce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQLite / SQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(SQL = Structured Query Language)&lt;/td&gt;
&lt;td&gt;A complete relational database living in one file, &lt;code&gt;progress.db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple-Choice Question&lt;/td&gt;
&lt;td&gt;One of the two quiz question types (the other is true/false)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Central Processing Unit&lt;/td&gt;
&lt;td&gt;Where Whisper runs — no graphics card required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;int8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8-bit integer (quantisation)&lt;/td&gt;
&lt;td&gt;Storing model weights as small integers: smaller, faster, accurate enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Artificial Intelligence&lt;/td&gt;
&lt;td&gt;Software performing tasks that normally need human intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Application Programming Interface&lt;/td&gt;
&lt;td&gt;The endpoints the Study Hub and dashboard call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;The vector index in the privacy-receipts table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database-Oriented Operating System&lt;/td&gt;
&lt;td&gt;The durable-workflow library whose checkpoints live in PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server-Side Request Forgery&lt;/td&gt;
&lt;td&gt;The attack class the URL importer guards against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PNG / PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Portable Network Graphics / Portable Document Format&lt;/td&gt;
&lt;td&gt;Two of the mindmap export formats (plus Markdown)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SVG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scalable Vector Graphics&lt;/td&gt;
&lt;td&gt;The browser drawing format behind the interactive mindmap rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Next steps:&lt;/strong&gt; clone
and read along — the README maps the full architecture, and every claim in this series can be checked directly against the code in &lt;code&gt;backend/&lt;/code&gt;. And if you want the deep-dive versions of these topics, the
picks up where this tour ends.&lt;/p&gt;</description></item><item><title>Part 7 · Gamifying Learning: 25 Badges, Idle-Gap Sessions, and a 90-Day Heatmap</title><link>https://aretascodes.dev/blog/gamifying-learning-badges-heatmap/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/blog/gamifying-learning-badges-heatmap/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Part of a series on building
. Previously:
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;All abbreviations are fully explained in the appendix at the bottom of the page.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;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: &lt;strong&gt;students who showed up consistently learned. Students who didn&amp;rsquo;t, didn&amp;rsquo;t.&lt;/strong&gt; Talent, prior knowledge, even motivation on any given day — all of it was downstream of attendance.&lt;/p&gt;
&lt;p&gt;CogniVault&amp;rsquo;s &amp;ldquo;Dashboard&amp;rdquo; tab is a small attempt to engineer for that. It&amp;rsquo;s not a Duolingo streak panic machine. It&amp;rsquo;s three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hero stats&lt;/strong&gt; — total study time, total sessions, current streak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;25 achievement badges&lt;/strong&gt; — auto-tracked across chat, quizzes, workshops, flashcards, mindmaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A 90-day activity heatmap&lt;/strong&gt; — GitHub-style, five purple intensity levels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The whole thing is a small SQLite table-set and a few React components. The interesting part isn&amp;rsquo;t the code, though — it&amp;rsquo;s the design calls.&lt;/p&gt;
&lt;h2 id="idle-gap-sessions"&gt;Idle-gap sessions&lt;/h2&gt;
&lt;p&gt;The hardest question turned out to be the simplest-sounding: &lt;strong&gt;what counts as one study session?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The naive answer is &amp;ldquo;anything bookended by open/close of the app.&amp;rdquo; That&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;The answer I landed on: a session ends when you&amp;rsquo;ve been &lt;strong&gt;idle for 15 minutes&lt;/strong&gt;. Ask a question, idle for 16 minutes — that&amp;rsquo;s one session. Come back, ask another — that starts a new one. The threshold is configurable via &lt;code&gt;STUDY_SESSION_IDLE_GAP_SECONDS=900&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The clock keys off &lt;strong&gt;chat messages&lt;/strong&gt; — the conversational core of studying in CogniVault. Every message either extends the open session (bumping its &lt;code&gt;ended_at&lt;/code&gt; timestamp and message count) or, if the gap since the last activity exceeds the threshold, closes it implicitly and opens a new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified from backend/services/progress_tracker.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idle_gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;most_recent_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;idle_gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# same session continues&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;open_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# new session begins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two writes per message. A session&amp;rsquo;s duration is &lt;code&gt;ended_at - started_at&lt;/code&gt;, which means &amp;ldquo;total time&amp;rdquo; reflects &lt;em&gt;engaged&lt;/em&gt; time, not &amp;ldquo;had a tab open.&amp;rdquo; 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.)&lt;/p&gt;
&lt;h2 id="25-badges-not-250"&gt;25 badges, not 250&lt;/h2&gt;
&lt;p&gt;Most gamified apps absolutely flood you with achievements. There&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;I capped CogniVault at &lt;strong&gt;25&lt;/strong&gt;, split across the five activity surfaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10 for &lt;strong&gt;chat &amp;amp; study habits&lt;/strong&gt; (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)&lt;/li&gt;
&lt;li&gt;4 for &lt;strong&gt;quizzes&lt;/strong&gt; (first quiz, a perfect score, passing on advanced difficulty, 10 quizzes)&lt;/li&gt;
&lt;li&gt;4 for &lt;strong&gt;workshops&lt;/strong&gt; (first outline, first completed lesson, first completed workshop, 5 completed)&lt;/li&gt;
&lt;li&gt;4 for &lt;strong&gt;flashcards&lt;/strong&gt; (first deck, 50 card flips, fully mastering a deck, 5 decks)&lt;/li&gt;
&lt;li&gt;3 for &lt;strong&gt;mindmaps&lt;/strong&gt; (first mindmap, first export, 5 mindmaps)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each badge has a one-line unlock criterion that&amp;rsquo;s auto-evaluated on relevant events. Nothing manual, nothing the user has to &amp;ldquo;claim.&amp;rdquo; They just appear.&lt;/p&gt;
&lt;p&gt;And the definitions aren&amp;rsquo;t code at all — they&amp;rsquo;re &lt;strong&gt;data&lt;/strong&gt;. All 25 live in a single JSON file, each entry naming the metric it watches and the target to hit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;card_reviewer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Card Reviewer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;icon&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;🃏&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;metric&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;total_card_flips&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;target&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A single evaluator reads the current stats, compares every definition against its metric, diffs against already-earned badges, and inserts new unlocks into &lt;code&gt;progress.db&lt;/code&gt;. Adding badge number 26 means adding a JSON entry, not writing new logic. Several badges form ladders — each knows which badge is its &amp;ldquo;next level,&amp;rdquo; which powers the detail view&amp;rsquo;s nudge toward the next goal.&lt;/p&gt;
&lt;h2 id="the-heatmap"&gt;The heatmap&lt;/h2&gt;
&lt;p&gt;The 90-day heatmap is the part I&amp;rsquo;m proudest of, and also the simplest. It&amp;rsquo;s a 13×7 grid of cells, one per day, coloured by total study time that day.&lt;/p&gt;
&lt;p&gt;Five intensity levels:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;level 0 — no activity
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;level 1 — under 15 minutes (a quick check-in)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;level 2 — 15-60 minutes (a focused session)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;level 3 — 1-3 hours (substantial study)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;level 4 — 3+ hours (a marathon)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The data is conceptually one aggregation over the sessions table:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;study_sessions&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;now&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-90 days&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The backend zero-fills the missing days so the frontend always receives exactly 90 entries, and a small client-side function bins each day&amp;rsquo;s total into the five levels. Click any cell and a &lt;code&gt;DayDetailModal&lt;/code&gt; opens with that day&amp;rsquo;s numbers — study time, sessions, messages — plus any badges earned that day.&lt;/p&gt;
&lt;p&gt;The reason I love this component: it makes the &lt;em&gt;texture&lt;/em&gt; 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&amp;rsquo;ve been on a slow drift downward all month, or that the gap between your last &amp;ldquo;level 4 day&amp;rdquo; and today is longer than you thought. It reflects something the user can act on.&lt;/p&gt;
&lt;h2 id="what-i-deliberately-left-out"&gt;What I deliberately left out&lt;/h2&gt;
&lt;p&gt;Three things you&amp;rsquo;d find in most gamified apps, intentionally absent from CogniVault:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Streak panic.&lt;/strong&gt; No &amp;ldquo;your streak is in danger!&amp;rdquo; pop-up. No streak freeze rules. No yellow exclamation marks. The streak is shown — that&amp;rsquo;s the entirety of the feedback loop. If a user breaks their streak, they break their streak. Adults don&amp;rsquo;t need shaming UX.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Leaderboards.&lt;/strong&gt; This is a single-user, fully local app. There&amp;rsquo;s no global comparison. (And there shouldn&amp;rsquo;t be — leaderboards optimise the wrong thing for studying.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Confetti, fanfare, push notifications.&lt;/strong&gt; A newly earned badge shows up in the quiz results screen and on the dashboard grid. That&amp;rsquo;s the entire celebration. Anything bigger is theft of the user&amp;rsquo;s attention for the app&amp;rsquo;s benefit, not theirs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The general principle: &lt;strong&gt;measure what matters, surface it without nagging.&lt;/strong&gt; Notice that you came back. Reflect that back to you. Don&amp;rsquo;t pretend you care more than you do.&lt;/p&gt;
&lt;h2 id="what-the-dashboard-doesn-try-to-optimise"&gt;What the dashboard &lt;em&gt;doesn&amp;rsquo;t&lt;/em&gt; try to optimise&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;So the bar is deliberately low in exactly one place and high everywhere else. There is &lt;em&gt;one&lt;/em&gt; zero-effort badge — &amp;ldquo;First Question,&amp;rdquo; 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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total study time&lt;/strong&gt; — only accrues during active engagement, with idle-gap cutoffs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sessions&lt;/strong&gt; — adding more requires actually starting separate work periods.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Badges&lt;/strong&gt; — almost all require depth (100 messages, ace a quiz, master a deck, complete 5 workshops), not just touch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heatmap intensity&lt;/strong&gt; — needs sustained engagement on a given day.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="implementation-small-on-purpose"&gt;Implementation: small on purpose&lt;/h2&gt;
&lt;p&gt;The gamification core is three SQLite tables —&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;study_sessions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;message_events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sent_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;had_scope_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;had_attachments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;achievements_earned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;earned_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;— plus the JSON badge definitions, one evaluator module, and a handful of React components (&lt;code&gt;SummaryCards&lt;/code&gt;, &lt;code&gt;AchievementGrid&lt;/code&gt;, &lt;code&gt;ActivityHeatmap&lt;/code&gt;, &lt;code&gt;DayDetailModal&lt;/code&gt;). The same &lt;code&gt;progress.db&lt;/code&gt; file has since grown more tables for the Study Hub&amp;rsquo;s saved quizzes, workshops, decks, and mindmaps — but the badge-and-session machinery itself remains a couple hundred lines.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s nothing fancy in any of it. The dashboard works because the &lt;em&gt;design calls&lt;/em&gt; are right, not because the implementation is clever.&lt;/p&gt;
&lt;h2 id="takeaway"&gt;Takeaway&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re building a learning tool — or any tool that lives on user habit — gamify &lt;em&gt;consciously&lt;/em&gt;. 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.&lt;/p&gt;
&lt;p&gt;Or, more bluntly: don&amp;rsquo;t build Duolingo. Build a dashboard the user occasionally glances at and then closes, feeling slightly more inclined to keep going. That&amp;rsquo;s the whole job.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="appendix-abbreviations-in-this-post"&gt;Appendix: Abbreviations in this post&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abbreviation&lt;/th&gt;
&lt;th&gt;Full form&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User Experience&lt;/td&gt;
&lt;td&gt;How the product feels — the thing streak-panic mechanics sacrifice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ICT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Information and Communications Technology&lt;/td&gt;
&lt;td&gt;The subject I taught for eight years before going full-stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(SQL = Structured Query Language)&lt;/td&gt;
&lt;td&gt;A complete relational database living in one file, &lt;code&gt;progress.db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;The data format the 25 badge definitions live in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User Interface&lt;/td&gt;
&lt;td&gt;The dashboard surface: stats, grid, heatmap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt;
.&lt;/p&gt;</description></item></channel></rss>