Compare commits

..

7 Commits

Author SHA1 Message Date
28f8907259 T005-T006 2025-09-18 10:04:43 -06:00
8e82274d30 T004 2025-09-18 09:45:22 -06:00
e3de5342ce T003 2025-09-18 09:30:30 -06:00
b2103a7359 T002 2025-09-18 09:23:08 -06:00
4150af64bb T001 2025-09-18 00:44:16 -06:00
f058a1b03a feat: Add detailed task specifications for GlowTrack mood and habit wellbeing grid 2025-09-18 00:41:32 -06:00
2f096d0265 feat: Add GlowTrack mood and habit wellbeing grid specifications
- Introduced export schema for JSON data structure.
- Created renderer contract detailing canvas/SVG rendering requirements.
- Defined IndexedDB storage schema and migration strategies.
- Documented data model including entities and relationships.
- Developed implementation plan outlining execution flow and project structure.
- Provided quickstart guide for development environment setup.
- Compiled research documentation on performance, accessibility, and theming.
- Established feature specification with user scenarios and functional requirements.
2025-09-18 00:36:13 -06:00
51 changed files with 4357 additions and 0 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# EditorConfig helps maintain consistent coding styles
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

12
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,12 @@
// Root ESLint config for monorepo
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
env: { es2022: true, node: true, browser: true },
extends: ['eslint:recommended', 'prettier'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
ignorePatterns: ['node_modules/', 'dist/', 'build/', '.svelte-kit/', 'coverage/'],
overrides: [
{ files: ['**/*.cjs'], parserOptions: { sourceType: 'script' } },
],
};

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Node & package managers
node_modules/
.npm/
.pnpm-store/
.npmrc.local
# Logs & caches
*.log
pnpm-debug.log*
.npm-debug.log*
.yarn-debug.log*
.yarn-error.log*
.eslintcache
.cache/
# Build & dist
build/
dist/
coverage/
# SvelteKit / Vite
.svelte-kit/
.vite/
# Test artifacts (Vitest/Playwright)
playwright-report/
blob-report/
test-results/
coverage/
# Nix & direnv
result
.direnv/
.devenv/
# OS/editor
.DS_Store
Thumbs.db
.vscode/
.idea/
# Misc
*.local
*.swp
*.swo

5
.npmrc Normal file
View File

@@ -0,0 +1,5 @@
shamefully-hoist=false
prefer-workspace-packages=true
workspace-concurrency=3
auto-install-peers=true
fund=false

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
pnpm-lock.yaml
coverage
build
dist
.svelte-kit

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"tabWidth": 2,
"useTabs": false
}

13
apps/web/README.md Normal file
View File

@@ -0,0 +1,13 @@
# GlowTrack Web (SvelteKit)
Minimal scaffold created for T005.
Scripts:
- pnpm dev — start dev server
- pnpm build — build static site (adapter-static)
- pnpm preview — preview built site
After T006/T007, Tailwind and tests will be wired.
# GlowTrack Web App
Placeholder for SvelteKit PWA app. See specs in `specs/001-glowtrack-a-mood/`.

31
apps/web/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "@glowtrack/web",
"private": true,
"version": "0.0.0",
"type": "module",
"description": "GlowTrack SvelteKit web app (adapter-static)",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"test": "echo \"No unit tests yet (see T007)\" && exit 0",
"test:e2e": "echo \"No e2e tests yet (see T007, T012T015)\" && exit 0"
},
"dependencies": {
"svelte": "^4.2.18"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"svelte-preprocess": "^5.1.4",
"tailwindcss": "^3.4.14",
"@tailwindcss/forms": "^0.5.9",
"svelte": "^4.2.18",
"typescript": "^5.5.4",
"vite": "^5.1.0"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

42
apps/web/src/app.css Normal file
View File

@@ -0,0 +1,42 @@
:root {
--bg: #0b0b10;
--fg: #e5e7eb;
--muted: #9ca3af;
color-scheme: dark;
}
html, body, #svelte {
height: 100%;
}
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
background: var(--bg);
color: var(--fg);
}
a { color: inherit; }
.container {
max-width: 960px;
padding: 1rem;
margin: 0 auto;
}
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 6px;
}
.tile {
aspect-ratio: 1 / 1;
border-radius: 6px;
background: #111827;
box-shadow: 0 0 10px rgba(56, 189, 248, 0.15) inset;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

11
apps/web/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
apps/web/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import '../app.css';
</script>
<svelte:head>
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0b0b10" />
<title>GlowTrack</title>
</svelte:head>
<div class="min-h-dvh bg-zinc-950 text-zinc-100">
<slot />
</div>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
const days = Array.from({ length: 35 });
</script>
<main class="container">
<h1>GlowTrack</h1>
<p class="muted">Mood & Habit wellbeing grid — SvelteKit scaffold.</p>
<section class="grid" aria-label="demo grid">
{#each days as _, i}
<div class="tile" aria-label={`day ${i + 1}`} />
{/each}
</section>
</main>

View File

View File

@@ -0,0 +1,20 @@
{
"name": "GlowTrack",
"short_name": "GlowTrack",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0b10",
"theme_color": "#0b0b10",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

18
apps/web/svelte.config.js Normal file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: preprocess({ postcss: true }),
kit: {
adapter: adapter({
fallback: '200.html'
}),
// Service worker wiring comes in T008
paths: {
// supports GitHub Pages-like hosting later; keep default for now
}
}
};
export default config;

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,js,svelte,ts}',
],
theme: {
extend: {},
},
plugins: [require('@tailwindcss/forms')],
};

19
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true,
"baseUrl": ".",
"types": ["svelte", "vite/client", "@sveltejs/kit"],
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*", "vite.config.ts", "svelte.config.js"],
"exclude": ["node_modules", "dist", "build", ".svelte-kit"]
}

6
apps/web/vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1758070117,
"narHash": "sha256-uLwwHFCZnT1c3N3biVe/0hCkag2GSrf9+M56+Okf+WY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9b7f2ff62b35f711568b1f0866243c7c302028d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

54
flake.nix Normal file
View File

@@ -0,0 +1,54 @@
{
description = "GlowTrack: reproducible devShell and app build outputs";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
# Developer shell with Node.js LTS, pnpm, git, jq
devShells.default = pkgs.mkShell {
packages = builtins.attrValues {
inherit (pkgs) nodejs_20 pnpm git jq;
};
# Tip: uncomment Playwright bits below once tests are wired (T007)
# packages = builtins.attrValues { inherit (pkgs) nodejs_20 pnpm git jq playwright-driver playwright-browsers; };
# shellHook = ''
# export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-browsers}
# '';
};
# Package: static site output from apps/web
# This derivation copies a prebuilt site (build/ or dist/) from apps/web if present.
# It does not run networked installs, keeping the build pure. The actual build
# step is performed via pnpm in the dev shell (see T005/T030 for CI wiring).
packages.app = pkgs.stdenvNoCC.mkDerivation {
pname = "glowtrack-app";
version = "0.0.0";
src = ./.;
buildPhase = "true";
installPhase = ''
mkdir -p "$out"
if [ -d apps/web/build ]; then
cp -r apps/web/build/* "$out/"
elif [ -d apps/web/dist ]; then
cp -r apps/web/dist/* "$out/"
else
echo "No prebuilt app output detected. After scaffolding (T005), run 'pnpm -C apps/web build' to populate build/ or dist/." > "$out/README.txt"
fi
'';
};
# Make `.#default` point to the app package for convenience
packages.default = self.packages.${system}.app;
}
);
}

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "glowtrack",
"private": true,
"version": "0.0.0",
"description": "GlowTrack monorepo root (pnpm workspaces)",
"scripts": {
"build": "pnpm -r --if-present build",
"test": "pnpm -r --if-present test",
"lint": "pnpm -r --if-present lint",
"typecheck": "pnpm -r --if-present typecheck || pnpm -r --if-present check",
"format": "pnpm -r --if-present format"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"svelte-check": "^3.8.5"
},
"packageManager": "pnpm@9.0.0"
}

View File

@@ -0,0 +1,3 @@
# @glowtrack/storage
Placeholder for storage package (IndexedDB, models, export/import).

View File

@@ -0,0 +1,12 @@
# Storage Import & Migration Benchmark
Benchmarks JSON import and basic IndexedDB migrations for GlowTrack.
## Use
- Open `import-benchmark.html` in a browser.
- Click "Generate 3-year JSON" then "Import".
- Observe timings for days vs entries transactions.
## Goals
- Determine chunking and transaction strategies.
- Inform UX for progress and error handling.

View File

@@ -0,0 +1,130 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GlowTrack IndexedDB Import Benchmark</title>
<style> body { font-family: system-ui, sans-serif; padding: 1rem; } textarea { width: 100%; height: 120px; } </style>
</head>
<body>
<h1>IndexedDB Import Benchmark</h1>
<button id="seed">Generate 3-year JSON</button>
<label>Chunk size: <input id="chunk" type="number" min="10" max="5000" value="500" /></label>
<button id="import">Import</button>
<button id="runMatrix">Run Matrix</button>
<button id="download">Download JSON Report</button>
<button id="wipe">Wipe DB</button>
<pre id="log"></pre>
<script>
const log = (...args) => (document.getElementById('log').textContent += args.join(' ') + '\n');
function openDB(version = 1) {
return new Promise((resolve, reject) => {
const req = indexedDB.open('glowtrack', version);
req.onupgradeneeded = (e) => {
const db = req.result;
if (!db.objectStoreNames.contains('settings')) db.createObjectStore('settings');
if (!db.objectStoreNames.contains('habits')) db.createObjectStore('habits', { keyPath: 'id' });
if (!db.objectStoreNames.contains('days')) db.createObjectStore('days', { keyPath: 'date' });
if (!db.objectStoreNames.contains('entries')) {
const s = db.createObjectStore('entries', { keyPath: 'id' });
s.createIndex('by_date', 'date');
s.createIndex('by_habit', 'habitId');
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function randomUUID(){return crypto.randomUUID?crypto.randomUUID():('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16)}))}
function generateData(years=3){
const today = new Date();
const start = new Date(today);
start.setDate(start.getDate() - years*365);
const days = [];
const habits = [
{ id: 'h1', type: 'positive', label: 'Walk', defaultWeight: 1, archived: false },
{ id: 'h2', type: 'negative', label: 'Late snack', defaultWeight: 1, archived: false }
];
for(let d=new Date(start); d<=today; d.setDate(d.getDate()+1)){
const date = d.toISOString().slice(0,10);
const entries = [];
if (Math.random() < 0.6) entries.push({ id: randomUUID(), type:'positive', habitId:'h1', label:'Walk', weight:1, timestamp:new Date(d).toISOString(), date });
if (Math.random() < 0.3) entries.push({ id: randomUUID(), type:'negative', habitId:'h2', label:'Late snack', weight:1, timestamp:new Date(d).toISOString(), date });
const hue = Math.floor(Math.random()*360);
const intensity = Math.random();
days.push({ date, mood:{hue,intensity}, entries });
}
return { version: '1.0.0', app:{name:'GlowTrack', version:'0.0.0'}, exportedAt: new Date().toISOString(), data:{ settings:{}, habits, days } };
}
async function importJSON(db, json, chunk=Infinity){
const tx1 = db.transaction(['days'], 'readwrite');
const daysStore = tx1.objectStore('days');
const t0 = performance.now();
let i=0; const days = json.data.days;
while(i < days.length){
const slice = days.slice(i, Math.min(i+chunk, days.length));
for(const day of slice){ await new Promise((res, rej)=>{ const r = daysStore.put({date:day.date, mood:day.mood}); r.onsuccess=()=>res(); r.onerror=()=>rej(r.error); }); }
i += slice.length;
}
await new Promise((res, rej)=>{ tx1.oncomplete=()=>res(); tx1.onerror=()=>rej(tx1.error); });
const t1 = performance.now();
const tx2 = db.transaction(['entries'], 'readwrite');
const entriesStore = tx2.objectStore('entries');
i=0; const entries = json.data.days.flatMap(d=>d.entries);
while(i < entries.length){
const slice = entries.slice(i, Math.min(i+chunk, entries.length));
for(const e of slice){ await new Promise((res, rej)=>{ const r = entriesStore.put(e); r.onsuccess=()=>res(); r.onerror=()=>rej(r.error); }); }
i += slice.length;
}
await new Promise((res, rej)=>{ tx2.oncomplete=()=>res(); tx2.onerror=()=>rej(tx2.error); });
const t2 = performance.now();
return { daysMs: t1 - t0, entriesMs: t2 - t1 };
}
document.getElementById('seed').onclick = () => {
window.generated = generateData(3);
log('Generated days:', window.generated.data.days.length);
};
const report = { runs: [] };
document.getElementById('import').onclick = async () => {
if (!window.generated) { log('Generate first.'); return; }
const db = await openDB(1);
const chunk = +document.getElementById('chunk').value || Infinity;
const res = await importJSON(db, window.generated, chunk);
log('Imported. Chunk:', chunk, 'Days(ms):', res.daysMs.toFixed(0), 'Entries(ms):', res.entriesMs.toFixed(0));
report.runs.push({ date: new Date().toISOString(), chunk, ...res });
db.close();
};
document.getElementById('runMatrix').onclick = async () => {
if (!window.generated) { log('Generate first.'); return; }
const chunks = [100, 250, 500, 1000, 2000, 5000];
for(const ch of chunks){
await new Promise((r)=>{ const del = indexedDB.deleteDatabase('glowtrack'); del.onsuccess=r; del.onerror=r; });
const db2 = await openDB(1);
const res = await importJSON(db2, window.generated, ch);
report.runs.push({ date: new Date().toISOString(), chunk: ch, ...res });
log('Matrix:', ch, 'Days(ms):', res.daysMs.toFixed(0), 'Entries(ms):', res.entriesMs.toFixed(0));
db2.close();
}
alert('Matrix complete');
};
document.getElementById('download').onclick = () => {
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'storage-benchmark.json'; a.click();
URL.revokeObjectURL(url);
};
document.getElementById('wipe').onclick = async () => {
await new Promise((res, rej)=>{ const r = indexedDB.deleteDatabase('glowtrack'); r.onsuccess=res; r.onerror=()=>rej(r.error); });
log('DB wiped');
};
</script>
</body>
</html>

3
packages/theme/README.md Normal file
View File

@@ -0,0 +1,3 @@
# @glowtrack/theme
Placeholder for theme tokens and CSS variables.

3
packages/viz/README.md Normal file
View File

@@ -0,0 +1,3 @@
# @glowtrack/viz
Placeholder for visualization/renderer package.

View File

@@ -0,0 +1,15 @@
# Viz FPS Harness
A simple Canvas benchmark to evaluate tile rendering performance (FPS), DPR scaling, and animation cost for GlowTrack.
## Use
- Open `index.html` in a browser (mobile preferred).
- Adjust:
- Tiles: 30365
- Animate: on/off
- Device Pixel Ratio: 13
- Observe FPS and rendering smoothness.
## Goals
- Validate 60 fps target for typical grids.
- Identify breakpoints for fallbacks (reduced motion, static render).

157
packages/viz/poc/index.html Normal file
View File

@@ -0,0 +1,157 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GlowTrack Viz FPS Harness</title>
<style>
html, body { height: 100%; margin: 0; font-family: system-ui, sans-serif; }
#controls { padding: 8px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
#fps { font-weight: bold; }
canvas { display: block; width: 100%; height: calc(100% - 48px); background: #0b0b10; }
</style>
</head>
<body>
<div id="controls">
<label>Tiles: <input id="tiles" type="number" min="30" max="365" value="365" /></label>
<label>Animate: <input id="animate" type="checkbox" checked /></label>
<label>Device Pixel Ratio: <input id="dpr" type="number" min="1" max="3" step="0.25" value="1" /></label>
<button id="runBench">Run Bench (30,90,180,365)</button>
<button id="download">Download JSON Report</button>
<span id="fps">FPS: --</span>
</div>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const tilesInput = document.getElementById('tiles');
const animateInput = document.getElementById('animate');
const dprInput = document.getElementById('dpr');
const fpsEl = document.getElementById('fps');
let tiles = +tilesInput.value;
let animate = animateInput.checked;
let dpr = +dprInput.value;
const state = [];
function seed() {
state.length = 0;
const today = new Date();
for (let i = 0; i < tiles; i++) {
const hue = (i * 11) % 360;
const net = Math.sin(i) * 2; // -2..2
state.push({ hue, net, pos: (i % 5), neg: (i % 3) });
}
}
function resize() {
const rect = canvas.getBoundingClientRect();
canvas.width = Math.floor(rect.width * dpr);
canvas.height = Math.floor(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function draw(time) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const padding = 8, gap = 2;
const cols = Math.ceil(Math.sqrt(tiles));
const rows = Math.ceil(tiles / cols);
const tileSize = Math.min((canvas.width/dpr - padding*2 - gap*(cols-1)) / cols,
(canvas.height/dpr - padding*2 - gap*(rows-1)) / rows);
let x = padding, y = padding;
for (let i = 0; i < tiles; i++) {
const s = state[i];
const glow = Math.max(0, Math.min(1, (s.net + (animate ? 0.5*Math.sin(time/500 + i) : 0)) / 3));
const lum = 20 + glow * 65; // luminance range 20..85
ctx.fillStyle = `hsl(${s.hue}deg 80% ${lum}%)`;
ctx.fillRect(x, y, tileSize, tileSize);
// static overlay for negative
if (s.neg > 0) {
ctx.globalAlpha = 0.08 + 0.03 * s.neg;
for (let n = 0; n < 10; n++) {
const rx = x + Math.random() * tileSize;
const ry = y + Math.random() * tileSize;
ctx.fillStyle = '#000000';
ctx.fillRect(rx, ry, 1, 1);
}
ctx.globalAlpha = 1;
}
// glyphs
ctx.fillStyle = '#fff';
for (let p = 0; p < s.pos; p++) {
ctx.fillRect(x + 2 + p*3, y + tileSize - 4, 2, 2);
}
ctx.fillStyle = '#bbb';
for (let n = 0; n < s.neg; n++) {
ctx.fillRect(x + tileSize - 2 - n*3, y + tileSize - 4, 2, 2);
}
x += tileSize + gap;
if ((i+1) % cols === 0) { x = padding; y += tileSize + gap; }
}
}
let last = performance.now();
let frames = 0;
const report = { runs: [] };
function loop(ts) {
draw(ts);
frames++;
if (ts - last > 1000) {
fpsEl.textContent = `FPS: ${frames}`;
frames = 0; last = ts;
}
requestAnimationFrame(loop);
}
tilesInput.oninput = () => { tiles = +tilesInput.value; seed(); };
animateInput.onchange = () => { animate = animateInput.checked; };
dprInput.oninput = () => { dpr = +dprInput.value; resize(); };
window.addEventListener('resize', resize);
seed(); resize(); requestAnimationFrame(loop);
async function runOnce(tCount){
tiles = tCount; tilesInput.value = tCount; seed();
return new Promise((resolve) => {
let f = 0; const tStart = performance.now();
const orig = requestAnimationFrame;
function sample(ts){
f++;
if (ts - tStart >= 1500) {
resolve({ tiles: tCount, fps: Math.round((f*1000)/(ts - tStart)) });
} else { orig(sample); }
}
orig(sample);
});
}
document.getElementById('runBench').onclick = async () => {
const dprPrev = dpr; const animPrev = animate;
const dprs = [1, 2];
const counts = [30, 90, 180, 365];
const results = [];
for (const d of dprs) {
dpr = d; dprInput.value = d; resize();
for (const a of [false, true]) {
animate = a; animateInput.checked = a;
for (const c of counts) {
const r = await runOnce(c);
results.push({ dpr: d, animate: a, ...r });
}
}
}
dpr = dprPrev; dprInput.value = dprPrev; animate = animPrev; animateInput.checked = animPrev; resize();
report.runs.push({ date: new Date().toISOString(), results });
alert('Benchmark complete');
};
document.getElementById('download').onclick = () => {
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'viz-benchmark.json'; a.click();
URL.revokeObjectURL(url);
};
</script>
</body>
</html>

2534
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- 'apps/*'
- 'packages/*'

1
result Symbolic link
View File

@@ -0,0 +1 @@
/nix/store/a7d43vv7g79mi2da7b977vqy0cqnaa45-glowtrack-app-0.0.0

View File

@@ -0,0 +1,75 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://glowtrack.app/schema/export.json",
"title": "GlowTrack Export",
"type": "object",
"required": ["version", "app", "exportedAt", "data"],
"properties": {
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
"app": {
"type": "object",
"required": ["name", "version"],
"properties": {
"name": { "const": "GlowTrack" },
"version": { "type": "string" }
}
},
"exportedAt": { "type": "string", "format": "date-time" },
"data": {
"type": "object",
"required": ["settings", "habits", "days"],
"properties": {
"settings": { "type": "object" },
"habits": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "label", "defaultWeight", "archived"],
"properties": {
"id": { "type": "string" },
"type": { "enum": ["positive", "negative"] },
"label": { "type": "string" },
"icon": { "type": ["string", "null"] },
"defaultWeight": { "type": "number", "minimum": 0 },
"archived": { "type": "boolean" }
}
}
},
"days": {
"type": "array",
"items": {
"type": "object",
"required": ["date", "mood", "entries"],
"properties": {
"date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
"mood": {
"type": "object",
"required": ["hue", "intensity"],
"properties": {
"hue": { "type": "number", "minimum": 0, "maximum": 360 },
"intensity": { "type": "number", "minimum": 0, "maximum": 1 },
"note": { "type": ["string", "null"] }
}
},
"entries": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "habitId", "label", "weight", "timestamp"],
"properties": {
"id": { "type": "string" },
"type": { "enum": ["positive", "negative"] },
"habitId": { "type": "string" },
"label": { "type": "string" },
"weight": { "type": "number", "minimum": 0 },
"timestamp": { "type": "string", "format": "date-time" }
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
# Renderer Contract — Canvas/SVG
## Purpose
Render a grid of DayTiles with mood hue, glow intensity from net habit score, negative overlay static, and glyph counts.
## Inputs
- containerSize: { width: px, height: px, devicePixelRatio }
- days: DayTile[] (ordered by date)
- theme: palette + CSS variables
- options:
- showLegend: boolean
- pngScale: number (for export)
## Outputs
- On-screen render at 60 fps target
- Exported PNG at screen resolution (pngScale defaults to 1.0)
## Rules
- Base hue from mood.hue; luminance curve f(netScore) with easing and clamping
- Negative entries: apply subtle static texture overlay (non-hue-altering)
- Glyphs: ticks = count(positive), dots = count(negative)
- No hue change from overlays; only luminance/texture affected
## Performance
- Batch draw tiles; minimize layout/paint; avoid per-frame allocations
- Prefer Canvas for tiles; SVG for glyph overlays and interactive focus rings
- Ensure keyboard focus indicators meet WCAG AA contrast
## Accessibility
- Keyboard navigable tiles (tab/arrow); ARIA labels describing day, mood, counts
- High-contrast theme variant; color-blind palettes via CSS variables

View File

@@ -0,0 +1,34 @@
# Storage Schema — IndexedDB (idb)
## DB Name
- glowtrack
## Versioning
- Start at version 1; bump on schema changes
- Provide forward-only migrations for v1 → v2 → ...
## Object Stores
- settings (key: 'singleton')
- value: GridSettings
- habits (keyPath: 'id')
- indexes: by_type (type)
- days (keyPath: 'date')
- value: DayTile without entries
- entries (keyPath: 'id')
- indexes:
- by_date (date)
- by_habit (habitId)
## Transactions
- Log habit: readwrite on entries, days (update netScore)
- Edit/delete: readwrite on entries, days
- Import JSON: version check, bulk put within a single transaction per store
## Migrations
- v1: create stores and indexes above
- Future: add derived caches (e.g., monthly aggregates) — must be rebuildable
## Data Integrity
- Enforce unique DayTile.date
- Recompute DayTile.netScore after entry mutations
- Maintain referential link of HabitEntry.habitId to habits store

View File

@@ -0,0 +1,79 @@
# Data Model — GlowTrack
## Entities
### WellbeingGrid
- id: string (stable UUID)
- createdAt: ISO datetime
- updatedAt: ISO datetime
- settings: GridSettings
- days: DayTile[]
### GridSettings
- startDate: ISO date
- endDate: ISO date
- theme: string (palette id)
- colorBlindMode: 'none' | 'protanopia' | 'deuteranopia' | 'tritanopia'
- export: ExportSettings
### ExportSettings
- pngScale: number (1.0 = screen resolution)
- includeLegend: boolean
### DayTile
- date: ISO date (YYYY-MM-DD)
- mood: Mood
- entries: HabitEntry[]
- netScore: number (derived: sum(positive weights) - sum(negative weights))
### Mood
- hue: number (0360)
- intensity: number (01)
- note?: string
### HabitEntry
- id: string (UUID)
- type: 'positive' | 'negative'
- habitId: string
- label: string
- weight: number (default 1; negative weights discouraged — use type)
- timestamp: ISO datetime
### HabitDefinition
- id: string (UUID)
- type: 'positive' | 'negative'
- label: string
- icon?: string (for UI glyphs)
- defaultWeight: number (default 1)
- archived: boolean
## Relationships
- WellbeingGrid has many DayTile
- DayTile has many HabitEntry
- HabitEntry references HabitDefinition via habitId
## Derived/Display Rules
- Base hue = mood.hue
- Glow luminance = function(netScore) with gentle easing; clamp to range
- Negative entries add subtle static texture overlay
- Glyphs: ticks for positive count; dots for negative count
## Validation Rules
- Dates must be valid ISO; no duplicates for DayTile.date
- HabitEntry.weight > 0; type determines sign for net score
- netScore recomputed on add/update/delete of entries
- Schema version must be present in exported JSON
## JSON Export Structure (high level)
- version: string (semver)
- app: { name: 'GlowTrack', version: string }
- exportedAt: ISO datetime
- data: { settings, habits: HabitDefinition[], days: DayTile[] }
## IndexedDB Stores (overview)
- stores:
- settings (key: 'singleton')
- habits (key: id)
- days (key: date)
- entries (key: id, index: date, index: habitId)
- versioning: bump on schema change; write migrations per version

View File

@@ -0,0 +1,195 @@
# Implementation Plan: GlowTrack — Mood & Habit Wellbeing Grid
**Branch**: `001-glowtrack-a-mood` | **Date**: 18 September 2025 | **Spec**: `/home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/spec.md`
**Input**: Feature specification from `/home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/spec.md`
## Execution Flow (/plan command scope)
```
1. Load feature spec from Input path
→ If not found: ERROR "No feature spec at {path}"
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
→ Set Structure Decision based on project type
3. Fill the Constitution Check section based on the content of the constitution document.
4. Evaluate Constitution Check section below
→ If violations exist: Document in Complexity Tracking
→ If no justification possible: ERROR "Simplify approach first"
→ Update Progress Tracking: Initial Constitution Check
5. Execute Phase 0 → research.md
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
6. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, `GEMINI.md` for Gemini CLI, `QWEN.md` for Qwen Code or `AGENTS.md` for opencode).
7. Re-evaluate Constitution Check section
→ If new violations: Refactor design, return to Phase 1
→ Update Progress Tracking: Post-Design Constitution Check
8. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
9. STOP - Ready for /tasks command
```
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
- Phase 2: /tasks command creates tasks.md
- Phase 3-4: Implementation execution (manual or via tools)
## Summary
GlowTrack is a browser-first PWA that turns daily mood and habit inputs into a vibrant, artistic wellbeing grid. Each day is a tile whose mood defines base hue; positive habits increase glow, negative habits reduce it and add a subtle static overlay, with glyphs indicating counts. The app is offline-first, private (no accounts, no servers), and supports PNG export of the grid and JSON import/export for full data portability.
Technical approach: SvelteKit + TypeScript using `@sveltejs/adapter-static` (no backend). Local-first storage with IndexedDB via `idb`. Grid visualization via a lightweight Canvas/SVG renderer tuned for mobile. Styling with Tailwind CSS and CSS variables for theming (including color-blind modes). PWA with SvelteKit service worker and minimal runtime cache. Tooling and reproducible builds via Nix flakes (devShell and build). CI executes Nix builds, Vitest unit tests, and Playwright smoke tests.
## Technical Context
**Language/Version**: TypeScript (ES2022), SvelteKit
**Primary Dependencies**: SvelteKit, `@sveltejs/adapter-static`, `idb`, Tailwind CSS, `@tailwindcss/forms`, Vite, `svelte-check`, ESLint, Prettier
**Storage**: IndexedDB via `idb` with versioned schema; JSON import/export
**Testing**: Vitest (unit), Playwright (e2e smoke), svelte-check (types)
**Target Platform**: Modern browsers (mobile and desktop) as a PWA, offline-capable
**Project Type**: Web (frontend-only PWA)
**Performance Goals**: 60 fps interactions and animations on mid-range mobile; initial interactive < 2s on 3G-like; maintain smooth canvas/SVG rendering for up to 365 tiles
**Constraints**: No servers or accounts; full offline read/write; local-only data; small bundle; accessible (WCAG AA); keyboard-first interactions
**Scale/Scope**: Single-user, local dataset (13 years of daily entries, dozens of habits)
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
The constitution file contains placeholders and no explicit project principles. Adopt default gates aligned with simplicity and test-first intent:
- Tests-first mindset captured in plan (Vitest/Playwright before features land)
- Simplicity: no backend, minimal dependencies, static hosting
- Observability via simple structured logs in dev and explicit export/import contracts
Initial Constitution Check: PASS (no violations detected)
## Project Structure
### Documentation (this feature)
```
specs/[###-feature]/
├── plan.md # This file (/plan command output)
├── research.md # Phase 0 output (/plan command)
├── data-model.md # Phase 1 output (/plan command)
├── quickstart.md # Phase 1 output (/plan command)
├── contracts/ # Phase 1 output (/plan command)
└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
```
### Source Code (repository root)
```
apps/
└── web/ # SvelteKit PWA (UI & routes)
packages/
├── viz/ # Tile/grid renderer (Canvas/SVG)
├── storage/ # IndexedDB schema, migrations, import/export
└── theme/ # Palettes, CSS variables, color-blind modes
tools/
└── ci/ # CI configs/scripts (Nix, Vitest, Playwright)
```
**Structure Decision**: Custom web monorepo (frontend-only) with shared packages for viz, storage, and theme per requirements
## Phase 0: Outline & Research
1. **Extract unknowns from Technical Context** above:
- For each NEEDS CLARIFICATION → research task
- For each dependency → best practices task
- For each integration → patterns task
2. **Generate and dispatch research agents**:
```
For each unknown in Technical Context:
Task: "Research {unknown} for {feature context}"
For each technology choice:
Task: "Find best practices for {tech} in {domain}"
```
3. **Consolidate findings** in `research.md` using format:
- Decision: [what was chosen]
- Rationale: [why chosen]
- Alternatives considered: [what else evaluated]
**Output**: research.md with all NEEDS CLARIFICATION resolved
## Phase 1: Design & Contracts
*Prerequisites: research.md complete*
1. **Extract entities from feature spec** → `data-model.md`:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
2. **Generate API contracts** from functional requirements:
- For each user action → endpoint
- Use standard REST/GraphQL patterns
- Output OpenAPI/GraphQL schema to `/contracts/`
3. **Generate contract tests** from contracts:
- One test file per endpoint
- Assert request/response schemas
- Tests must fail (no implementation yet)
4. **Extract test scenarios** from user stories:
- Each story → integration test scenario
- Quickstart test = story validation steps
5. **Update agent file incrementally** (O(1) operation):
- Run `.specify/scripts/bash/update-agent-context.sh copilot` for your AI assistant
- If exists: Add only NEW tech from current plan
- Preserve manual additions between markers
- Update recent changes (keep last 3)
- Keep under 150 lines for token efficiency
- Output to repository root
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
## Phase 2: Task Planning Approach
*This section describes what the /tasks command will do - DO NOT execute during /plan*
**Task Generation Strategy**:
- Load `.specify/templates/tasks-template.md` as base
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
- Each contract → contract test task [P]
- Each entity → model creation task [P]
- Each user story → integration test task
- Implementation tasks to make tests pass
**Ordering Strategy**:
- TDD order: Tests before implementation
- Dependency order: Models before services before UI
- Mark [P] for parallel execution (independent files)
**Estimated Output**: 25-30 numbered, ordered tasks in tasks.md
**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
## Phase 3+: Future Implementation
*These phases are beyond the scope of the /plan command*
**Phase 3**: Task execution (/tasks command creates tasks.md)
**Phase 4**: Implementation (execute tasks.md following constitutional principles)
**Phase 5**: Validation (run tests, execute quickstart.md, performance validation)
## Complexity Tracking
*Fill ONLY if Constitution Check has violations that must be justified*
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
## Progress Tracking
*This checklist is updated during execution flow*
**Phase Status**:
- [x] Phase 0: Research complete (/plan command)
- [x] Phase 1: Design complete (/plan command)
- [ ] Phase 2: Task planning complete (/plan command - describe approach only)
- [ ] Phase 3: Tasks generated (/tasks command)
- [ ] Phase 4: Implementation complete
- [ ] Phase 5: Validation passed
**Gate Status**:
- [x] Initial Constitution Check: PASS
- [x] Post-Design Constitution Check: PASS
- [x] All NEEDS CLARIFICATION resolved
- [ ] Complexity deviations documented
---
*Based on Constitution v2.1.1 - See `/memory/constitution.md`*

View File

@@ -0,0 +1,35 @@
# Quickstart — GlowTrack
## Prerequisites
- Nix installed (flakes enabled)
## Dev Environment
- Enter dev shell:
- nix develop
- Install JS deps (first time):
- pnpm install
- Run typecheck and lints:
- pnpm run svelte-check
- pnpm run lint
## Run App (dev)
- Start web app:
- pnpm run dev
- Open in browser (URL printed by dev server)
## Build (static site)
- Build via Nix flake:
- nix build .#app
- Or via pnpm:
- pnpm run build
## Tests
- Unit tests:
- pnpm run test
- E2E smoke (headed/CI):
- pnpm run test:e2e
## Export/Import
- Export JSON (in-app): Settings → Export → JSON
- Import JSON: Settings → Import → select file
- Export PNG: Share/Export → PNG (screen resolution)

View File

@@ -0,0 +1,79 @@
# Research — GlowTrack
## Decisions
- Framework: SvelteKit + TypeScript with `@sveltejs/adapter-static` (no backend)
- Storage: IndexedDB via `idb`; JSON import/export with versioning
- Visualization: Lightweight Canvas/SVG hybrid renderer optimized for mobile
- Styling: Tailwind CSS with CSS variables; color-blind friendly palettes
- Accessibility: Target WCAG AA; keyboard-first interactions
- PWA: SvelteKit service worker with minimal runtime cache for full offline
- Tooling/Reproducibility: Nix flake provides devShell and build
- CI: Nix build + Vitest unit tests + Playwright smoke tests
- Hosting: GitHub Pages or Netlify (static hosting) from Nix build output
## Rationales
- SvelteKit offers SSR/SSG flexibility and first-class PWA support while remaining light for static export
- `idb` simplifies IndexedDB usage and supports versioned migrations for local-first data
- Canvas/SVG hybrid provides high-performance drawing with crisp glyph overlays and accessibility-friendly fallbacks
- Tailwind accelerates consistent UI while CSS variables enable theming and low-cost runtime adjustments
- Keeping everything client-side preserves privacy and enables offline-by-default usage
- Nix ensures reproducible dev environments and CI builds across machines
## Alternatives Considered
- React/Vite: viable but SvelteKit yields smaller bundles and simpler reactivity
- LocalStorage/WebSQL: insufficient for structured data and migrations; IndexedDB preferred
- Pure Canvas or pure SVG: hybrid approach chosen to balance performance with resolution-independent elements
- Service worker libraries (e.g., Workbox): SvelteKits built-in SW is sufficient given minimal caching needs
## Open Questions (Resolved by requirements/spec)
- Export format: PNG at screen resolution for sharing/wallpaper
- Sharing: JSON export/import only; no hosted links or image sharing by app
- Tile visual rules: Base hue by mood; net habit score controls luminance; negative = static overlay; glyph counts
---
## Additional Research Backlog (Prioritized)
1) Canvas/SVG performance on mobile (P1)
- Goal: 60 fps for 150365 tiles with glow/overlay; define rendering budget and fallbacks
- Deliverable: Benchmark results, thresholds, viz package notes
- Method: Build FPS harness; test mid/low-end Android + iOS
2) IndexedDB schema + import/migration performance (P1)
- Goal: Reliable imports for multi-year datasets; smooth migrations; responsive UI
- Deliverable: Import chunk size, transaction strategy, migration template, storage footprint
- Method: Seed 3-year dataset; time import/migration; measure DB size/quota
3) Accessibility palettes and glyph legibility (P1)
- Goal: WCAG AA contrast; color-blind safe palettes; legible glyphs at small sizes
- Deliverable: Approved palettes, CSS vars, glyph sizing spec
- Method: Simulators + Axe checks; user testing if possible
4) PWA SW update + offline write safety (P1)
- Goal: Safe app update with pending writes and schema changes
- Deliverable: SW update policy, cache versioning, migration + rollback checklist
- Method: Offline simulation during schema bump; verify no data loss
5) PNG export fidelity and limits (P2)
- Goal: Fast, reliable PNG export with crisp glyphs; cap size to prevent OOM
- Deliverable: Use toBlob/toDataURL guidance, max dimensions, UX for progress/errors
- Method: Export month/year grids; measure time/memory/file size
6) Keyboard-first grid navigation + SR UX (P2)
- Goal: Intuitive keyboard nav with correct ARIA; SR speaks mood and counts
- Deliverable: Roving tabindex model, ARIA roles/labels, SR strings
- Method: Prototype navigation + Playwright+Axe tests
7) Tailwind + CSS variables theming (P2)
- Goal: Minimal CSS output with dynamic theming; safe class generation
- Deliverable: Theming tokens and example; purge-safe patterns
- Method: Prototype theme switcher; inspect bundle size
### Active Research Tasks (links)
- 01: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/01-viz-performance.md
- 02: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/02-indexeddb-imports-migrations.md
- 03: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/03-a11y-palettes-glyphs.md
- 04: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/04-pwa-sw-offline-safety.md
- 05: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/05-png-export-fidelity.md
- 06: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/06-keyboard-sr-grid.md
- 07: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/research/07-tailwind-css-vars.md

View File

@@ -0,0 +1,41 @@
# Research: Canvas/SVG Performance on Mobile
## Hypothesis
We can sustain 60 fps for 150365 tiles with glow/overlay on mid-range devices by batching draws and minimizing per-frame allocations.
## Questions
- FPS across tile counts (30, 90, 180, 365) and DPR (1.0, 2.0, 3.0)
- Cost of static overlay and glyph rendering
- Breakpoints for reduced motion or static rendering
## Method
- Use `/packages/viz/poc/index.html` harness
- Test devices: low-end Android, mid Android, iPhone
- Record FPS and memory; profile with devtools
## Test Matrix (fill during runs)
| DPR | Animate | Tiles | FPS | Notes |
|-----|---------|-------|-----|-------|
| 1 | false | 30 | | |
| 1 | false | 90 | | |
| 1 | false | 180 | | |
| 1 | false | 365 | | |
| 1 | true | 30 | | |
| 1 | true | 90 | | |
| 1 | true | 180 | | |
| 1 | true | 365 | | |
| 2 | false | 30 | | |
| 2 | false | 90 | | |
| 2 | false | 180 | | |
| 2 | false | 365 | | |
| 2 | true | 30 | | |
| 2 | true | 90 | | |
| 2 | true | 180 | | |
| 2 | true | 365 | | |
## Acceptance
- 5560 fps at 180 tiles on mid devices
- Document fallback thresholds and settings
## Deliverables
- Metrics table; recommendations for renderer design

View File

@@ -0,0 +1,31 @@
# Research: IndexedDB Imports & Migrations
## Hypothesis
Chunked transactions and store-per-entity design yield responsive imports and safe migrations for 3-year datasets.
## Questions
- Import throughput (items/sec) for days vs entries
- Optimal chunk size per transaction
- Quota usage for 3-year dataset
- Migration time and error handling patterns
## Method
- Use `/packages/storage/poc/import-benchmark.html`
- Measure timings, DB size; simulate version bump
## Test Matrix (fill during runs)
| Chunk | Days(ms) | Entries(ms) | Notes |
|-------|----------|-------------|-------|
| 100 | | | |
| 250 | | | |
| 500 | | | |
| 1000 | | | |
| 2000 | | | |
| 5000 | | | |
## Acceptance
- Import < 5s for 3-year synthetic dataset on mid device
- Migrations complete without UI lockup and preserve data
## Deliverables
- Import strategy, migration template, data integrity checklist

View File

@@ -0,0 +1,20 @@
# Research: Accessibility Palettes & Glyph Legibility
## Hypothesis
CSS variable-driven palettes can satisfy WCAG AA while remaining legible for common color-blind conditions.
## Questions
- Contrast ratios for default and high-contrast themes
- Visibility of mood hue under protanopia/deuteranopia/tritanopia
- Glyph legibility at small tile sizes
## Method
- Prototype palettes; run through color-blind simulators and Axe
- Validate glyph size/contrast on sample tiles
## Acceptance
- AA contrast for focus indicators and glyphs
- Verified palettes for three color-blind modes
## Deliverables
- Approved palettes, token list, glyph sizing guidance

View File

@@ -0,0 +1,19 @@
# Research: PWA SW Updates & Offline Write Safety
## Hypothesis
With cache versioning and cautious update prompts, we can avoid data loss during app updates and schema bumps while offline.
## Questions
- Best timing for SW activation without disrupting writes
- Handling schema migrations when a stale SW is cached
- Safe rollback strategy
## Method
- Simulate offline state; bump schema; observe SW activation and data integrity
## Acceptance
- No data loss or corruption across update/migration
- Clear UX for update available and post-update state
## Deliverables
- SW update policy, cache naming, migration/rollback checklist

View File

@@ -0,0 +1,19 @@
# Research: PNG Export Fidelity & Limits
## Hypothesis
Canvas toBlob can reliably export year-scale grids at screen resolution with acceptable memory and time on mid devices.
## Questions
- toBlob vs toDataURL performance/memory
- Max safe export dimensions before OOM
- Glyph/text crispness at DPR > 1
## Method
- Extend viz harness to export PNG; measure time and size
## Acceptance
- Export < 2s for year grid at scale 1.0 on mid device
- Document caps and UX messaging for long exports
## Deliverables
- Export pipeline guidance; caps; progress/error UX recommendations

View File

@@ -0,0 +1,19 @@
# Research: Keyboard-first Grid & Screen Reader UX
## Hypothesis
A roving tabindex grid with appropriate ARIA roles can provide intuitive keyboard navigation and informative SR output.
## Questions
- Best roles: grid vs listgrid vs table
- Roving tabindex vs per-cell tab stops
- SR announcement strings for date, mood, positive/negative counts
## Method
- Create minimal prototype; test with Axe and at least one SR (NVDA/VoiceOver)
## Acceptance
- Arrow navigation works; SR reads context and counts
- Meets WCAG AA navigability and focus visibility
## Deliverables
- ARIA mapping and strings; nav model; test checklist

View File

@@ -0,0 +1,18 @@
# Research: Tailwind & CSS Variables Theming
## Hypothesis
We can keep CSS output small while supporting dynamic theming via CSS variables and Tailwind utilities.
## Questions
- Mapping CSS variables to Tailwind utilities without bloating CSS
- Safe patterns for dynamic classes and purge
- Runtime theme switching performance cost
## Method
- Prototype theme tokens and a toggle; inspect CSS size
## Acceptance
- Minimal CSS growth; smooth theme switching
## Deliverables
- Token design, Tailwind config guidance, sample components

View File

@@ -0,0 +1,103 @@
# Feature Specification: GlowTrack — Mood & Habit Wellbeing Grid
**Feature Branch**: `001-glowtrack-a-mood`
**Created**: 18 September 2025
**Status**: Draft
**Input**: User description: "GlowTrack — a mood and habit tracker that transforms daily inputs into a vibrant wellbeing grid. Each tile in the grid represents a day, glowing brighter as positive habits are built and dimming or distorting when negative habits are logged. The goal is to make personal growth feel rewarding, visual, and artistic, rather than like a clinical spreadsheet. Unlike typical habit apps that only encourage building routines, GlowTrack also supports tracking bad habits users want to reduce or quit. This makes the grid an honest reflection of both struggles and progress: good habits make the day shine, while harmful ones visibly dull the pattern. Over time, users see their grid evolve into a mosaic of resilience, balance, and self-improvement. The emphasis is on keeping GlowTrack lightweight, private, and beautiful. It should work directly in the browser as a PWA, require no accounts or servers, and let users export/share their wellbeing grids as personal artwork. The focus is motivation through aesthetics — turning daily check-ins into a canvas of personal growth."
## Execution Flow (main)
```
1. Parse user description from Input
→ If empty: ERROR "No feature description provided"
2. Extract key concepts from description
→ Identify: actors, actions, data, constraints
3. For each unclear aspect:
→ Mark with [NEEDS CLARIFICATION: specific question]
4. Fill User Scenarios & Testing section
→ If no clear user flow: ERROR "Cannot determine user scenarios"
5. Generate Functional Requirements
→ Each requirement must be testable
→ Mark ambiguous requirements
6. Identify Key Entities (if data involved)
7. Run Review Checklist
→ If any [NEEDS CLARIFICATION]: WARN "Spec has uncertainties"
→ If implementation details found: ERROR "Remove tech details"
8. Return: SUCCESS (spec ready for planning)
```
---
## User Scenarios & Testing
### Primary User Story
A user opens GlowTrack in their browser and is greeted by a visually engaging grid representing their days. Each day, the user logs both positive and negative habits. As positive habits are built, the corresponding day's tile glows brighter; negative habits cause the tile to dim or distort. Over time, the user sees their grid evolve into a unique mosaic reflecting both their struggles and progress. The user can export or share their grid as personal artwork, all without creating an account or sharing data with a server.
### Acceptance Scenarios
1. **Given** a new user, **When** they open GlowTrack, **Then** they see an empty wellbeing grid ready for input.
2. **Given** a day in the grid, **When** the user logs a positive habit, **Then** the tile glows brighter.
3. **Given** a day in the grid, **When** the user logs a negative habit, **Then** the tile dims or distorts.
4. **Given** a completed grid, **When** the user chooses to export/share, **Then** the grid is saved as personal artwork.
5. **Given** a user, **When** they use GlowTrack, **Then** no account or server interaction is required.
### Edge Cases
- What happens if a user logs both positive and negative habits for the same day?
Conflicting habits in a single tile: The tile uses mood as the base hue. Glow intensity is based on the net habit score, where positive habits add glow and negative habits reduce it. Negative habits also add a subtle static overlay. Small glyphs indicate counts (ticks for positives, dots for negatives). Mood hue always remains clear, with overlays only affecting luminance or texture.
- How does the system handle days with no input?
a dim square.
- What if the user wants to edit or delete a habit entry?
allow editing.
- How is privacy maintained if the user shares their grid?
Export formats: Export is supported as PNG in screen resolution (suitable for sharing and wallpaper use), the user is responsible by whom he shares their grid, and is not hosted on any invasive servers.
## Requirements
### Functional Requirements
- **FR-001**: System MUST allow users to log both positive and negative habits for each day.
- **FR-002**: System MUST visually represent each day as a tile in a grid, with brightness and distortion reflecting habit quality.
- **FR-003**: Users MUST be able to export/share their wellbeing grid as personal artwork.
- **FR-004**: System MUST operate fully in-browser as a PWA, with no account or server required.
- **FR-005**: System MUST ensure user data is private and stored locally.
- **FR-006**: System MUST allow users to edit or delete habit entries for any day.
- **FR-007**: System MUST allow users to customize which habits are tracked.
- **FR-008**: System MUST provide a visually engaging, artistic interface for motivation.
- **FR-009**: System MUST allow users to reset or clear their grid if desired.
- **FR-010**: System MUST allow users to view their progress over time as a mosaic.
- **FR-011**: System MUST support offline usage.
- **FR-012**: System MUST allow users to select which days to display (e.g., week, month, year).
- **FR-013**: System MUST provide guidance or onboarding for first-time users.
- **FR-014**: System MUST allow users to share their grid without exposing personal habit details. Sharing is limited to JSON export. Users can back up, move, or import their full data through this format. No images or public links are generated automatically.
### Key Entities
- **DayTile**: Represents a single day in the grid; attributes include date, brightness, distortion, and habit entries.
- **HabitEntry**: Represents a logged habit; attributes include type (positive/negative), description, and timestamp.
- **WellbeingGrid**: Represents the user's overall grid; attributes include collection of DayTiles, export status, and visual style.
---
## Review & Acceptance Checklist
### Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
### Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
---
## Execution Status
- [x] User description parsed
- [x] Key concepts extracted
- [x] Ambiguities marked
- [x] User scenarios defined
- [x] Requirements generated
- [x] Entities identified
- [x] Review checklist passed
- [ ] Review checklist passed

View File

@@ -0,0 +1,253 @@
# Tasks: GlowTrack — Mood & Habit Wellbeing Grid
Input: /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/
Prerequisites: plan.md (required), research.md, data-model.md, contracts/
## Execution Flow (main)
1) Load plan.md from feature directory → Extract tech stack, libraries, structure
2) Load optional docs: data-model.md (entities), contracts/* (contract tests), research.md (decisions), quickstart.md (integration scenarios)
3) Generate tasks by category: Setup → Tests → Core → Integration → Polish
4) Apply rules: Different files = [P] parallel, same file = sequential; Tests before implementation (TDD)
5) Number T001…; define dependencies; include parallel examples and Agent Task commands
Paths below are absolute to this repo.
---
## Phase 3.1: Setup
- [X] T001 Create monorepo layout
- Create directories:
- /home/jawz/Development/Projects/GlowTrack/apps/web
- /home/jawz/Development/Projects/GlowTrack/packages/storage
- /home/jawz/Development/Projects/GlowTrack/packages/viz
- /home/jawz/Development/Projects/GlowTrack/packages/theme
- /home/jawz/Development/Projects/GlowTrack/tools/ci
- Add placeholder README.md in each new folder.
- Dependencies: none
- [X] T002 Initialize Nix flake (devShell + build outputs)
- Create /home/jawz/Development/Projects/GlowTrack/flake.nix providing:
- devShell with: nodejs (LTS 20+), pnpm, git, playwright browsers (via optional separate task), jq
- packages.app building static site from /apps/web (pnpm build)
- Add .envrc (optional) to auto-enter devShell
- Dependencies: T001
- [X] T003 Initialize pnpm workspaces
- Create /home/jawz/Development/Projects/GlowTrack/package.json (private workspace root) with scripts: lint, test, build, typecheck, format
- Create /home/jawz/Development/Projects/GlowTrack/pnpm-workspace.yaml listing apps/* and packages/*
- Dependencies: T001
- [X] T004 Configure linting, formatting, types
- Root: .editorconfig, .eslintrc.cjs, .prettierrc, .prettierignore, .npmrc (pnpm settings)
- Add dev deps: eslint, eslint-config-prettier, prettier, typescript, svelte-check
- Dependencies: T003
- [X] T005 Scaffold SvelteKit PWA (adapter-static)
- In /home/jawz/Development/Projects/GlowTrack/apps/web create a SvelteKit app:
- package.json, svelte.config.js (adapter-static), vite.config.ts, tsconfig.json
- src/app.d.ts, src/app.css, src/routes/+layout.svelte, src/routes/+page.svelte (hello grid)
- static/manifest.webmanifest (minimal)
- Add scripts: dev, build, preview, test, test:e2e, check
- Dependencies: T003, T004
- [X] T006 Tailwind CSS setup
- apps/web: tailwind.config.cjs, postcss.config.cjs; integrate @tailwindcss/forms
- Wire Tailwind into src/app.css and +layout.svelte
- Dependencies: T005
- [ ] T007 Vitest + Playwright test harness
- apps/web: vitest config (vitest + svelte), playwright.config.ts with basic smoke project
- Root CI scripts in tools/ci (stub) and package scripts wiring
- Dependencies: T005
- [ ] T008 PWA service worker wiring (SvelteKit)
- Enable service worker in SvelteKit config and add minimal SW handler
- Ensure static asset caching strategy is defined (runtime-minimal)
- Dependencies: T005
## Phase 3.2: Tests First (TDD) — MUST FAIL before 3.3
Contract files from /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/ → contract tests [P]
- [ ] T009 [P] Contract test: export JSON schema
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/tests/contract/export.spec.ts
- Use Ajv to validate object from exportToJson() against export.schema.json at /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/export.schema.json
- Expect failure until export service implemented
- Dependencies: T007
- [ ] T010 [P] Contract test: IndexedDB storage schema
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/tests/contract/schema.spec.ts
- Open DB via openDb() and assert stores/indexes per /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/storage.schema.md
- Expect failure until DB module/migrations implemented
- Dependencies: T007
- [ ] T011 [P] Contract test: renderer API
- Create /home/jawz/Development/Projects/GlowTrack/packages/viz/tests/contract/renderer.spec.ts
- Assert renderGrid(container, days, theme, options) exists and draws required layers per /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/renderer.md
- Expect failure until viz renderer implemented
- Dependencies: T007
Integration scenarios from quickstart.md → e2e smoke tests [P]
- [ ] T012 [P] E2E: mood + habits update tile
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.mood-habits.spec.ts
- Steps: open app → set day mood → add positive+negative habits → tile glow/luminance and glyphs update
- Dependencies: T007, T005
- [ ] T013 [P] E2E: export/import JSON roundtrip
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.export-import.spec.ts
- Steps: create few days → export JSON → clear DB → import JSON → grid identical
- Dependencies: T007, T005
- [ ] T014 [P] E2E: PNG export at screen resolution
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.png-export.spec.ts
- Steps: render month → export PNG (toBlob) → file within size/time budget
- Dependencies: T007, T005
- [ ] T015 [P] E2E: offline PWA works
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.offline.spec.ts
- Steps: install SW → go offline → app loads and writes mood/entries; on reconnect, state persists
- Dependencies: T007, T008, T005
## Phase 3.3: Core Implementation (only after tests are failing)
From data-model.md → model creation tasks [P]
- [ ] T016 [P] Define TypeScript models
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/src/models.ts
- Export interfaces: WellbeingGrid, GridSettings, ExportSettings, DayTile, Mood, HabitEntry, HabitDefinition
- Dependencies: T009-T015 (tests exist), T003
- [ ] T017 [P] Implement IndexedDB schema v1
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/src/db.ts with openDb(name='glowtrack', version=1)
- Create stores: settings (key 'singleton'), habits (keyPath 'id', index by_type), days (keyPath 'date'), entries (keyPath 'id', indexes by_date, by_habit)
- Dependencies: T016, T010
- [ ] T018 [P] Implement export/import service
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/src/export.ts with exportToJson(), importFromJson()
- Ensure JSON conforms to export.schema.json (version, app, exportedAt, data)
- Dependencies: T016, T009, T017
- [ ] T019 [P] Compute helpers (netScore, derivations)
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/src/compute.ts implementing netScore, clamps, updates on entry CRUD
- Dependencies: T016
Renderer and theme [P]
- [ ] T020 [P] Renderer: minimal Canvas/SVG hybrid
- Create /home/jawz/Development/Projects/GlowTrack/packages/viz/src/renderer.ts exporting renderGrid(container, days, theme, options)
- Canvas tiles with glow luminance curve; SVG overlay for glyphs/focus rings
- Dependencies: T011, T016, T019
- [ ] T021 [P] Theme: CSS variables + palettes
- Create /home/jawz/Development/Projects/GlowTrack/packages/theme/src/tokens.css and /home/jawz/Development/Projects/GlowTrack/packages/theme/src/index.ts
- Provide color-blind modes: none, protanopia, deuteranopia, tritanopia
- Dependencies: T006
UI wiring
- [ ] T022 Minimal UI to edit and view grid
- apps/web components:
- /home/jawz/Development/Projects/GlowTrack/apps/web/src/lib/components/Grid.svelte (uses packages/viz)
- /home/jawz/Development/Projects/GlowTrack/apps/web/src/lib/components/DayEditor.svelte (set mood, add entries)
- /home/jawz/Development/Projects/GlowTrack/apps/web/src/routes/+page.svelte (compose editor + grid)
- Dependencies: T020, T021, T017, T019
- [ ] T023 Wire storage + export/import + PNG export
- Implement usage of openDb, exportToJson/importFromJson; add PNG export via renderer (toBlob)
- Add buttons: Export JSON, Import JSON, Export PNG
- Files: /home/jawz/Development/Projects/GlowTrack/apps/web/src/lib/actions/export.ts, import.ts, png.ts (or colocated in +page.svelte)
- Dependencies: T018, T020, T022
## Phase 3.4: Integration
- [ ] T024 Tailwind integration and base styles
- Ensure Tailwind classes purge safely; add base typography/forms styles in app.css
- Files: /home/jawz/Development/Projects/GlowTrack/apps/web/src/app.css, tailwind.config.cjs
- Dependencies: T006, T022
- [ ] T025 PWA manifest + SW behavior
- Ensure manifest.webmanifest, icons (placeholder), SW caching strategy aligned with offline write safety
- Files: /home/jawz/Development/Projects/GlowTrack/apps/web/static/manifest.webmanifest, src/service-worker.ts
- Dependencies: T008, T022
- [ ] T026 Basic logging and error UX
- Add simple structured console logs in dev; user-visible error toasts for import failures
- Files: /home/jawz/Development/Projects/GlowTrack/apps/web/src/lib/stores/toast.ts, components/Toast.svelte
- Dependencies: T022, T023
## Phase 3.5: Polish
- [ ] T027 [P] Unit tests for compute helpers
- Create /home/jawz/Development/Projects/GlowTrack/packages/storage/tests/unit/compute.spec.ts covering netScore, clamping, entry CRUD recompute
- Dependencies: T019
- [ ] T028 [P] Renderer perf sanity test
- Create /home/jawz/Development/Projects/GlowTrack/packages/viz/tests/perf/render.perf.spec.ts rendering 365 tiles under budget; assert frame time thresholds (coarse)
- Dependencies: T020
- [ ] T029 [P] Docs: READMEs and quickstart wiring
- Update /home/jawz/Development/Projects/GlowTrack/README.md and /home/jawz/Development/Projects/GlowTrack/apps/web/README.md; link to spec and tasks
- Dependencies: T022, T023
- [ ] T030 Final build + e2e pass via Nix
- Run nix build .#app and playwright e2e; attach artifacts
- Dependencies: T015, T025, T029
---
## Dependencies Summary
- Setup (T001T008) before Tests (T009T015)
- Tests before Core (T016T023)
- Models (T016) before Storage services (T017T019)
- Storage services before UI wiring (T022T023)
- Renderer (T020) and Theme (T021) before Grid UI (T022)
- PWA wiring (T008, T025) before Offline e2e (T015)
- Polish (T027T030) after Core/Integration
## Parallel Execution Examples
Launch independent [P] tasks together (different paths, no shared files):
```
Task: "T009 [P] Contract test: export JSON schema in /packages/storage/tests/contract/export.spec.ts"
Task: "T010 [P] Contract test: IndexedDB storage schema in /packages/storage/tests/contract/schema.spec.ts"
Task: "T011 [P] Contract test: renderer API in /packages/viz/tests/contract/renderer.spec.ts"
Task: "T012 [P] E2E mood+habits in /apps/web/tests/e2e/smoke.mood-habits.spec.ts"
Task: "T013 [P] E2E export/import in /apps/web/tests/e2e/smoke.export-import.spec.ts"
Task: "T014 [P] E2E PNG export in /apps/web/tests/e2e/smoke.png-export.spec.ts"
Task: "T027 [P] Unit tests compute in /packages/storage/tests/unit/compute.spec.ts"
Task: "T028 [P] Renderer perf test in /packages/viz/tests/perf/render.perf.spec.ts"
Task: "T029 [P] Docs updates in /README.md and /apps/web/README.md"
```
Agent Task commands (example grouping):
```
# Group 1: Contract + e2e tests (T009T015)
Task: "Create failing contract tests for export, storage, renderer"
Task: "Create failing e2e smoke tests for mood/habits, export/import, PNG, offline"
# Group 2: Models and services (T016T019)
Task: "Implement models.ts, db.ts, export.ts, compute.ts in packages/storage"
# Group 3: Viz + theme (T020T021)
Task: "Implement renderer.ts in packages/viz and tokens.css in packages/theme"
# Group 4: UI wiring (T022T026)
Task: "Build minimal UI and wire storage/export/PNG + PWA"
# Group 5: Polish and build (T027T030)
Task: "Add unit/perf tests, docs, and run final Nix build + e2e"
```
## Validation Checklist
- All contract files have corresponding tests (T009T011) → YES
- All entities have model tasks (T016) → YES
- All tests come before implementation → YES (Phase 3.2 precedes 3.3)
- [P] tasks are independent and avoid same-file edits → YES
- Each task specifies exact absolute file paths → YES
- No task with [P] modifies the same file as another [P] task → YES
Notes
- Use adapter-static for SvelteKit; no backend services
- Local-first via IndexedDB (idb) with versioned migrations
- Tailwind + CSS variables for theming and color-blind modes
- Reproducible dev via Nix; CI can reuse flake outputs

3
tools/ci/README.md Normal file
View File

@@ -0,0 +1,3 @@
# CI Tools
Placeholder for CI scripts and config.