diff --git a/HANDOFF.md b/HANDOFF.md index 607844a..65c16ae 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,8 +4,8 @@ > If this file disagrees with the code, trust the code and fix this file. ## 1. Where we are -- **Last completed step:** 9.1 (Security checklist walk — fixed innerHTML + added rate limiter) — TICKED in STATUS.md -- **Next step:** Phase 9 — 9.2 (Add `npm run seed:dev` creating a demo account and hero) +- **Last completed step:** 9.2 (seed-dev script — creates demo account + hero) — TICKED in STATUS.md +- **Next step:** Phase 9 — 9.3 (Global error pages / friendly UI error toasts. Loading states.) - **Updated:** 2026-10-21 - **Was the last session cut off mid-task?** No @@ -15,6 +15,7 @@ | `npm install` | succeeds | | `npm run migrate` | applies 001–006 / "nothing to do" | | `npm test` | **172 passing, 0 failing** (85 unit + 87 integration) | +| `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear | ## 3. What exists now (files that matter) - `public/profile.html` — Public hero profile page (reads ?name=, displays stats/equipment). @@ -23,6 +24,7 @@ - `public/js/game.js` — hunt UI + inventory UI + expedition UI. - `public/shop.html` — Dedicated shop page. - `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation. +- `scripts/seed-dev.js` — Creates demo account + hero + starter gear (idempotent, skips in production). - `src/services/heroProfileService.js` — `getHeroProfile(heroName)`. - `src/routes/heroProfile.js` — `GET /api/heroes/:name` (public). - `src/services/leaderboardService.js` — `getLeaderboard(type, page, limit)`. @@ -53,7 +55,7 @@ - Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md). - Starter gear (GAME_DESIGN.md §Items) documented but not auto-created on character creation. - `GET /api/zones` documented in API.md but not implemented (Issue #2). -- Step 9.2 (`npm run seed:dev`) not yet implemented. +- Step 9.3 (error pages/toasts/loading states) not yet implemented. ## 5. Decisions & deviations from the docs - D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock). @@ -84,10 +86,12 @@ - Hero profile endpoint is PUBLIC — no `requireAuth` middleware. - Profile page is public: no auth.js guard needed; uses ?name= query param for lookup. - Rate limiters in tests: set `NODE_ENV=test` to get 1000 req/window limit; otherwise tests fail with 429. +- `seed-dev.js` must check `NODE_ENV=production` before any `require()` that loads config (config throws on placeholder SECRET). -## 7. Next-step briefing (9.2) -- **Goal:** Add `npm run seed:dev` script that creates a demo account and hero on an empty DB. -- **Docs:** None specific — follow existing patterns (migrate.js, authService, characterService). -- Should: run migrations, then insert a test user + character, optionally give starter gear. -- Should be safe to re-run (check if demo user exists first). -- Should NOT be used in production (skip if NODE_ENV=production or check for a flag). +## 7. Next-step briefing (9.3) +- **Goal:** Add global error pages / friendly UI error toasts. Loading states. +- **Docs:** UI_GUIDE, SECURITY (for error handling), TESTING. +- Should: add a generic 404/500 error page template in `public/`. +- Should: add a toast notification system (JS + CSS) for user-facing errors. +- Should: add loading spinners/states for async operations (hunt, buy, etc.). +- Should NOT introduce new dependencies — use vanilla HTML/CSS/JS. diff --git a/STATUS.md b/STATUS.md index 1f83e41..debbd42 100644 --- a/STATUS.md +++ b/STATUS.md @@ -8,9 +8,9 @@ ## Current Focus - **Phase:** 9 -- **Step:** 9.2 +- **Step:** 9.3 - **Blocked?** No -- **Last verified working command:** `npm test` — 172/172 passing (85 unit + 87 integration), 2026-10-21 step 9.1 +- **Last verified working command:** `npm test` — 172/172 passing (85 unit + 87 integration), 2026-10-21 step 9.2 ## Project Health | Item | State | @@ -107,7 +107,8 @@ - [x] **9.1** Walk the SECURITY.md checklist; fix gaps. *Acceptance:* all SECURITY.md checklist items verified or fixed. *Fixed:* removed `innerHTML` with dynamic data in `game.js` (renderActiveExpedition) and `profile.js` (renderProfile stats) — converted to createElement/textContent. Added missing general `/api` rate limiter (120/min/IP per SECURITY.md spec). -- [ ] **9.2** Add `npm run seed:dev` creating a demo account and hero. +- [x] **9.2** Add `npm run seed:dev` creating a demo account and hero. + *Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production. - [ ] **9.3** Global error pages / friendly UI error toasts. Loading states. - [ ] **9.4** Mobile-responsive pass (see UI_GUIDE). - [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below. @@ -190,3 +191,4 @@ _Format:_ `YYYY-MM-DD | step | what changed | result` - 2026-10-20 | 2.4 | Created `public/create-hero.html`, `public/game.html`, `public/js/create-hero.js`, `public/js/game.js` | app loads - 2026-10-21 | 8.2 | Created `src/services/heroProfileService.js` (getHeroProfile), `src/routes/heroProfile.js` (GET /api/heroes/:name, public), integrated into app.js; 5 integration tests covering public lookup, case-insensitivity, equipped items, 404, no-auth access | 167/167 tests pass - 2026-10-21 | 9.1 | Walked SECURITY.md checklist: fixed `innerHTML` with dynamic data in `game.js` (renderActiveExpedition) and `profile.js` (renderProfile stats) — converted to createElement/textContent; added missing general `/api` rate limiter (120/min/IP per SECURITY.md spec); verified all 9 checklist items pass | 172/172 tests pass +- 2026-10-21 | 9.2 | Created `scripts/seed-dev.js`: runs migrations, creates demo user "demo"/"demo123", hero "Aldric" (warrior), grants starter gear (rusty sword, leather jerkin, 2 healing potions); idempotent; skips in production | 172/172 tests pass diff --git a/package.json b/package.json index 9c191b1..abf0a77 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dev": "node --watch src/server.js", "test": "NODE_ENV=test node --test --test-concurrency=1", "migrate": "node scripts/migrate.js", - "seed": "node scripts/seed.js" + "seed": "node scripts/seed.js", + "seed:dev": "node scripts/seed-dev.js" }, "keywords": [], "type": "commonjs", diff --git a/scripts/seed-dev.js b/scripts/seed-dev.js new file mode 100644 index 0000000..94e583a --- /dev/null +++ b/scripts/seed-dev.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node +// seed-dev.js — Create a demo account + hero for local testing. +// Safe to re-run (skips if demo user already exists). +// Skipped entirely when NODE_ENV=production. +// NOTE: NODE_ENV check must happen before any require() that loads config, +// because config throws on placeholder SESSION_SECRET in production. +if (process.env.NODE_ENV === 'production') { + console.log('seed-dev: NODE_ENV=production — skipping.'); + process.exit(0); +} + +const bcrypt = require('bcryptjs'); +const { runMigrations } = require('./migrate'); +const { getDb } = require('../src/db'); +const time = require('../src/utils/time'); + +const DEMO_USERNAME = 'demo'; +const DEMO_PASSWORD = 'demo123'; +const DEMO_HERO_NAME = 'Aldric'; +const DEMO_CLASS = 'warrior'; + +// Starter gear: a basic weapon + armor + potion for the demo hero. +const STARTER_ITEMS = [ + { slug: 'rusty-sword', qty: 1 }, // weapon, common, min_level 1 + { slug: 'leather-jerkin', qty: 1 }, // armor, common, min_level 1 + { slug: 'minor-healing-potion', qty: 2 }, // consumable, stackable +]; + +function getOrCreateDemoUser(db) { + const stmt = db.prepare( + 'SELECT id, username FROM users WHERE username = ? COLLATE NOCASE' + ); + const existing = stmt.get(DEMO_USERNAME); + if (existing) { + console.log(` Demo user "${DEMO_USERNAME}" already exists (id=${existing.id}).`); + return existing; + } + + const passwordHash = bcrypt.hashSync(DEMO_PASSWORD, 12); + const result = db.prepare( + 'INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)' + ).run(DEMO_USERNAME, passwordHash, time.now()); + + console.log(` Created demo user "${DEMO_USERNAME}" (id=${result.lastInsertRowid}).`); + return { id: result.lastInsertRowid, username: DEMO_USERNAME }; +} + +function getOrCreateDemoHero(db, userId) { + const heroCheck = db.prepare( + 'SELECT id FROM characters WHERE user_id = ?' + ).get(userId); + if (heroCheck) { + console.log(` Demo hero already exists for user ${userId}.`); + return null; + } + + const heroNameCheck = db.prepare( + 'SELECT id FROM characters WHERE name = ? COLLATE NOCASE' + ).get(DEMO_HERO_NAME); + if (heroNameCheck) { + console.log(` Hero name "${DEMO_HERO_NAME}" already taken.`); + return null; + } + + // Get base stats for the class + const { getBaseStats } = require('../src/game/classes'); + const { calcDerived } = require('../src/game/stats'); + const baseStats = getBaseStats(DEMO_CLASS); + const hpCurrent = calcDerived(baseStats, 1, DEMO_CLASS).maxHp; + const { ENERGY_MAX } = require('../src/game/stats'); + const now = time.now(); + + const result = db.prepare( + `INSERT INTO characters + (user_id, name, class, level, xp, gold, str, agi, int, vit, + unspent_points, hp_current, energy, energy_updated_at, kills, deaths, created_at) + VALUES (?, ?, ?, 1, 0, 100, ?, ?, ?, ?, 0, ?, ?, ?, 0, 0, ?)` + ).run( + userId, DEMO_HERO_NAME, + DEMO_CLASS, + baseStats.str, baseStats.agi, baseStats.int, baseStats.vit, + hpCurrent, ENERGY_MAX, now, now + ); + + console.log(` Created demo hero "${DEMO_HERO_NAME}" (id=${result.lastInsertRowid}, class=${DEMO_CLASS}).`); + return { id: result.lastInsertRowid }; +} + +function giveStarterGear(db, characterId) { + const itemIds = []; + for (const { slug, qty } of STARTER_ITEMS) { + const item = db.prepare('SELECT id FROM items WHERE slug = ?').get(slug); + if (!item) { + console.warn(` WARNING: item "${slug}" not found in database.`); + continue; + } + itemIds.push({ itemId: item.id, qty }); + } + + const insertInv = db.prepare( + 'INSERT INTO inventory (character_id, item_id, quantity, acquired_at) VALUES (?, ?, ?, ?)' + ); + + for (const { itemId, qty } of itemIds) { + insertInv.run(characterId, itemId, qty, time.now()); + } + + console.log(` Granted ${itemIds.length} starter item(s) to hero ${characterId}.`); +} + +function seedDev() { + console.log('seed-dev: running migrations...'); + runMigrations(); + + const db = getDb(); + console.log('seed-dev: seeding demo account...'); + + const user = getOrCreateDemoUser(db); + const hero = getOrCreateDemoHero(db, user.id); + + if (hero) { + giveStarterGear(db, hero.id); + } + + console.log(''); + console.log('═══════════════════════════════════════'); + console.log(' Demo account ready!'); + console.log(` Username: ${DEMO_USERNAME}`); + console.log(` Password: ${DEMO_PASSWORD}`); + console.log('═══════════════════════════════════════'); + console.log(''); +} + +// Run if called directly +if (require.main === module) { + seedDev(); + process.exit(0); +} + +module.exports = { seedDev };