Files
Lumotia/docs/superpowers/specs/2026-04-24-phase8-forgiving-gamification-design.md
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00

9.5 KiB
Raw Permalink Blame History

name, description, type, tags, created, status, phase, roadmap, author
name description type tags created status phase roadmap author
Phase 8 — Forgiving gamification (design) Design spec for Lumotia Phase 8. Surfaces today's completion count and a 7-day momentum sparkline on the Tasks header. No streaks, no grace days, no loss language. spec
spec
phase-8
lumotia
tasks
gamification
2026/04/24 approved 8 docs/roadmap/2026-04-23-lumotia-feature-complete-roadmap.md Wren (CORBEL's resident agent) on behalf of Jake Sames

Phase 8 — Forgiving gamification

Summary

Add two purely additive signals to the Tasks page header:

  1. Today's completion count as a soft-edged badge ("3 today") beside the "Tasks" title.
  2. 7-day momentum sparkline — a tiny inline 7-bar chart on the same line.

No streaks. No chains. No grace days. No loss language. Empty days render as baseline stubs (never gaps). Zero social comparison.

This is the final feature phase before polish (Phase 9) and release prep (Phase 10).

Counting semantics

A "completion today" is any task row whose done_at falls on the local calendar day and which was completed by explicit user action. Specifically:

  • Top-level tasks explicitly completed → count.
  • Subtasks explicitly completed (their own checkbox clicked) → count.
  • Parent tasks auto-completed via the complete_subtask_and_check_parent cascade → do not count. The user already got credit for the subtask they just ticked; the parent closing automatically must not double-count.
  • Uncompleting a task removes it from the count immediately (its row stops matching the query because done_at is nulled).

Day boundary is local-time midnight-to-midnight. done_at is stored as UTC; conversion uses SQLite's DATE(done_at, 'localtime').

Architecture

Data-model change — migration v13

Add a column to distinguish cascade-completed parents from explicit completions:

ALTER TABLE tasks ADD COLUMN auto_completed INTEGER NOT NULL DEFAULT 0;

CREATE INDEX idx_tasks_done_at_auto_completed
    ON tasks(done_at, auto_completed)
    WHERE done_at IS NOT NULL;

Partial index on done_at IS NOT NULL keeps the index small — only completed rows occupy it. Forward-only migration: existing completed rows default to auto_completed = 0 (they will count). We have no way to retro-detect pre-migration cascades; accept this one-time inaccuracy.

Rust changes

crates/storage/src/database.rs:

  • complete_subtask_and_check_parent: when the cascade UPDATE closes the parent, set auto_completed = 1 on that UPDATE. The subtask UPDATE itself leaves auto_completed at 0.
  • complete_task: unchanged — leaves auto_completed at 0.
  • uncomplete_task: the UPDATE already clears done = 0, done_at = NULL; also clear auto_completed = 0 to keep the row clean if completed again via a different path.
  • New function list_recent_completions(pool, days) returning Vec<DailyCompletionCount>:
pub struct DailyCompletionCount {
    pub day: String,   // "YYYY-MM-DD" local
    pub count: u32,
}

pub async fn list_recent_completions(
    pool: &SqlitePool,
    days: u32,
) -> Result<Vec<DailyCompletionCount>>;

Query groups matching rows by local-day and then left-joins against a generated N-day spine (computed in Rust) so empty days are explicit zeros, not missing entries. Result is ordered oldest → newest.

Tauri command: list_recent_completions_cmd(days: u32) in src-tauri, wired via the standard invoke pattern used by existing task commands.

Frontend changes

New store src/lib/stores/completionStats.svelte.ts:

// Exposes:
//   recentCompletions: DailyCompletionCount[]  — always length = days
//   todayCount: number                          — derived from last entry
//   refresh(): Promise<void>                    — invokes the Rust command

// Refreshes on:
//   - initial load (called from the same init path that runs loadTasks)
//   - after every task-mutation helper in page.svelte.ts
//     (completeTask / uncompleteTask / deleteTask / completeSubtask /
//     uncompleteSubtask / deleteSubtask) — call sites add a
//     completionStats.refresh() alongside the existing loadTasks() refresh
//   - window focus (for day rollover while the app stayed open overnight)

Why not hook off lumotia:task-completed alone: that event only fires on completion, not on uncomplete or delete, and relying on it would leave the badge stale after either of those paths. Refreshing at the mutation-helper level captures every path that can change today's count.

src/lib/pages/TasksPage.svelte header: Stacks to:

Tasks · 3 today          ▁▂▅▃▁▆▄
Add tasks manually…
  • Badge is an inline <span> after the "Tasks" title, matching title sizing but text-text-tertiary so it reads as subordinate. Hidden when todayCount === 0.
  • Sparkline is a ~80×16 px inline SVG of 7 <rect> bars. Zero-days render as a 1 px baseline stub. Same text-text-tertiary ink as the badge (SVG fill="currentColor" so the parent text colour drives it). Hidden when either the user has toggled it off or the full 7-day window is all zeros.
  • A single wrapper element around just the badge + sparkline carries aria-live="polite" (scope deliberately small so screen readers announce only the count change, not the rest of the header).

Accessibility labels:

  • Badge: aria-label="3 tasks completed today" (singular/plural aware).
  • Sparkline: aria-label="Tasks completed over the last 7 days: 0, 1, 3, 2, 0, 4, 3" with values from the current data.

Settings

New preference: showMomentumSparkline: boolean, default true, persisted through the existing saveSettings path.

Settings page — in the same visual cluster as the existing energy / match-my-energy toggles — one toggle:

  • Label: "Show momentum sparkline"
  • Helper: "A tiny chart of the last 7 days' completion counts, shown on the Tasks header. Never counts against you."

Only the sparkline respects the toggle. The badge is always on.

Invariants

  • Day boundary: local time.
  • Subtasks count on explicit completion; auto-cascade parents do not.
  • Uncomplete removes from count.
  • Sparkline is 7 bars always (even if all zero; the 7-day-all-zero case hides the whole sparkline rather than showing a flat row — different from a single intra-week zero which renders as a 1 px stub).
  • Brand-new install: no badge (count=0), no sparkline (all 7 days=0).
  • Zero loss language anywhere. No "you were away", no "streak broken", no "4 days ago".

Acceptance (from roadmap)

  • Complete 3 tasks today → header shows "3 today".
  • Open the app after 4 days off → no "you were away" framing; header reads today's count only; sparkline renders flat zero-stubs for the 4 away days, real bars for the other 3.
  • Cascade-complete a parent via its last subtask → badge increments by 1 (the subtask), not 2.
  • Uncomplete a task that was completed today → badge decrements by 1.
  • Toggle "Show momentum sparkline" off → sparkline disappears; badge stays.

Testing

Rust (cargo test in crates/storage)

  • list_recent_completions returns exactly days entries in date order.
  • Zero-days are present as {day, count: 0}, not absent.
  • auto_completed = 1 rows are excluded.
  • Manual subtask completion is counted.
  • uncomplete_task removes the row from the count.
  • Day boundary: insert done_at values at 23:59:59 UTC on day N-1 and 00:00:01 UTC on day N; query result correctly places each in the local day.
  • Migration v13: applies cleanly against a DB populated at v12; default of auto_completed = 0 on pre-existing rows verified.

Frontend (npm run check + vitest)

  • completionStats store refreshes on lumotia:task-completed event.
  • todayCount derives from the last entry.
  • Sparkline renders exactly 7 bars for a populated fixture.
  • Sparkline hidden when showMomentumSparkline === false.
  • Badge hidden when todayCount === 0.
  • Brand-new install (empty fixture): both elements hidden.

Manual dogfood (folds into Phase 10a QC)

  • Complete 3 tasks. Header reads "3 today".
  • Uncomplete one. Header reads "2 today".
  • Micro-step a task, complete its last subtask. Header increments by 1 (subtask), not 2.
  • Leave the app open past local midnight. On focus, badge resets to "0" (hidden) and sparkline shifts left.

Out of scope

  • Per-day tooltip on the sparkline (Phase 9).
  • Motion curves / enter animations (Phase 9).
  • Dark-mode colour tweaks beyond what the tertiary ink already gives us (Phase 9).
  • Any analytics, telemetry, export, or CSV download (not a v0.1 concern).
  • Per-list or per-bucket breakdowns (everything aggregates across all tasks).
  • Future-dated completions (can't happen — done_at is always now).

File map

  • crates/storage/src/migrations.rs — migration v13 block.
  • crates/storage/src/database.rscomplete_subtask_and_check_parent flag set; uncomplete_task flag clear; new list_recent_completions fn + DailyCompletionCount struct.
  • src-tauri/src/commands/... (whichever file holds task commands) — list_recent_completions_cmd Tauri command.
  • src/lib/stores/completionStats.svelte.ts — new.
  • src/lib/pages/TasksPage.svelte — header markup for badge + sparkline; subscribe to store.
  • src/lib/components/CompletionSparkline.svelte — new, dedicated SVG component (keeps TasksPage.svelte focused).
  • src/lib/types/app.tsDailyCompletionCount type; showMomentumSparkline added to settings type.
  • src/lib/pages/SettingsPage.svelte — toggle for showMomentumSparkline.

Effort estimate

Half day, matching the roadmap estimate.