Runs server tests (node:test), client tests (vitest), and build verification on every push and PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After saving all 12 mnemonics, players see their full codebook —
each letter with its Morse pattern (· —) and mnemonic phrase in
a grid layout. Two actions: Print/Save PDF (window.print) and
Start Playing. Operator name appears on the printed page.
Print CSS hides UI chrome and renders a clean 3-column grid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No email required — players pick male/female/mystery, get assigned
a humorous period-appropriate operator name (e.g. "Dusty Clicksworth",
"Ada Dottsworth", "The Phantom Sender"), can reroll, then hit
"Begin Transmitting!" for instant play. Email/magic link moved to
a collapsible section for cross-device access only.
- server: POST /api/auth/guest creates guest profile + session
- server/db: profiles.email now nullable, is_guest column added,
migration recreates table for existing installs
- client: names.ts with 15 names per gender category
- client: LoginPage redesigned — gender picker → name display → play
- client: useAuth gains setAuth() for direct session injection
- 55 server + 44 client tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace random word banks with hand-crafted mnemonic suggestions
for each letter. All phrases are meaningful (e.g. "he has his hat"
for H, "sun set sky" for S, "over only once" for O) and every word
starts with the target letter. Placeholder shows first suggestion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First word of each suggestion starts with the letter being learned
(e.g. "age WALK" for A). Validation requires at least one word to
start with the target letter. Added O/U/N/D/H words to fill gaps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dot words: ≤3 letters (was ≤2) — two-letter words like "go/it" were too limiting
- Dash words: ≥4 letters (unchanged in practice)
- Added DOT_WORDS and DASH_WORDS banks (~60 and ~200 words)
- suggestPhrases() generates 5 random valid suggestions per letter
- MnemonicBuilder shows clickable suggestion pills; "↻ new" refreshes them
- Selected suggestion highlights in gold; user can still type their own
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mailer.js: add requireTLS to force STARTTLS before AUTH on internal relay
- docker-compose.yml: use network_mode:bridge to reach 172.17.0.1:2587 SMTP relay
and switch to ${SMTP_HOST:-172.17.0.1}/${SMTP_PORT:-2587} defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Return 200 ok even if email send fails — token is saved in DB and
magic link is logged to console. Prevents 500 on transient SMTP issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- rejectUnauthorized: false (relay cert is for mail.keylinkit.net, not 172.17.0.1)
- connection/greeting/socket timeouts for reliability
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile: two-stage build (builder installs all deps + vite build; runtime copies dist + server)
- docker-compose.yml: 127.0.0.1:3004:8080, named volume for SQLite, env var interpolation
- .env.example: all required environment variables documented
- .dockerignore: exclude node_modules, dist, data, .env from build context
- vite.config.ts: outDir → ../client/dist, /api proxy to :3001 for dev
- .gitignore: remove Docker entries (files should be tracked for deployment)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AdminPage: password gate (sessionStorage), user table, stats banner
- Auto-login using stored sessionStorage password on mount
- App.tsx: route to admin when pathname === '/admin'
- Fix: remove redundant setPassword in auto-login effect (useState already seeds from sessionStorage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useMorseAudio: wraps MorseAudioEngine, exposes currentSignal for FlashArea
- useProgress: fetches /api/progress + /api/mnemonics, recordAnswer with local stats
- FlashArea: gold flash on signal, height varies dot/dash
- ScoreBar: level name, score, streak display
- PracticeCard: play button, answer input, correct/incorrect feedback with mnemonic
- GamePage: full practice loop, level-up modal at streak%10, anti-repeat history
- Fix: incorrect attempt recorded only once per challenge (no stat inflation on retries)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MnemonicBuilder: 12 NOVICE_LETTERS, real-time validatePhrase, save/skip
- OnboardingPage: wraps builder, passes token and onComplete
- App.tsx: post-auth fetch /api/mnemonics, route to onboarding if <12 saved
- Fix: validatePhrase called with letter (not Morse pattern) as second arg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useAuth: session token from URL/localStorage, /api/auth/me validation, logout
- LoginPage: email form with POST /api/auth/request, error handling
- App.tsx: use useAuth hook, LoginPage, 'sent' confirmation route
- Fix: auto-route to game covers both 'login' and 'sent' routes when authenticated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- server.js: Node HTTP server with all API routes, static file serving, SPA fallback
- Path traversal protection on static file serving
- Async handler wrapped with .catch() to prevent unhandled rejections
- readBody: size limit (1MB) + error handler
- letter validation: single [A-Z] char check on progress/mnemonics routes
- phrase length limit (500 chars) on POST /api/mnemonics
- requireAdmin reads ADMIN_PASSWORD at request time for testability
- logout uses session.token from requireAuth (not re-read from header)
- 51 server tests passing (auth, progress, admin routes + edge cases)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove module-level `let db` singleton; all query functions now close
over the local `instance` from their `initDb` call, preventing
cross-contamination when multiple callers each invoke `initDb`
- Add REFERENCES … ON DELETE CASCADE to profile_id in magic_tokens,
sessions, user_progress, user_mnemonics, and letter_stats so that
the already-enabled foreign_keys pragma is actually enforced by DDL
- recordAnswer: normalise letter to uppercase before storing stats so
lowercase 'a' and uppercase 'A' are never tracked separately
- updateLevel: call getOrCreateProgress first to avoid a silent no-op
UPDATE when no progress row exists yet
- saveMnemonic: replace SELECT-then-INSERT with an atomic UPSERT
(INSERT … ON CONFLICT DO UPDATE) and detect first-save by checking
whether created_at equals the current timestamp
- getSession: re-fetch the row after updating last_seen so the returned
object reflects the freshly written timestamp
- db.test.js: add tests for findProfileById, findProfileById (null),
updateProfileSeen, updateLevel (normal + no-row-yet), getAdminUsers,
and getAdminStats (21 tests total, all pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 4 (TDD): wrote failing tests first, then the full SQLite
database layer covering profiles, magic tokens, sessions, user progress,
mnemonics, and letter stats. All 14 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use pendingCancel flag (consumed once) to suppress stop-before-play while
allowing subsequent play() calls to proceed normally. Add afterEach timer
cleanup, re-play test, and behavioral assertion for off().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Complete Product Requirements Document with full technical specs
- MorseQuest adventure-based branding and visual identity
- All logos, icons, and brand assets ready for development
- Freemium business model ($9.95/year, first year free launch strategy)
- Ready for Keylink IT Forge platform deployment
Phase 1 MVP ready to begin (10-12 week timeline)