Wave 8 closes the high-value gaps from the original spec while staying
inside MVP scope (encounters/weapons/items still future work).
Spell generator (mirrors the NPC pattern end-to-end)
- src/lib/ai/types.ts: spellParamsSchema, spellResultSchema, Open5eSpell
- src/lib/ai/openrouter.ts: generateSpell using llama-3.3-70b:free for FREE
- src/lib/ai/kimi.ts: generateSpell using moonshot-v1-8k with json_schema
response_format for PRO
- src/pages/api/dm/ai/generate-spell.ts: auth → rate-limit → optional
/spells/ Open5e reference for balance → AI generate → Zod validate →
increment counter. Spells are returned to client and not persisted
(spec only requires NPC persistence for PRO).
- src/components/dm/AiSpellForm.astro: form (level, school, classes,
tone, suggested name) + inline result card rendering with copy-JSON
- src/components/dm/AiKindSwitcher.astro: segmented NPC | Spell control
wrapping both forms; default tab is NPC
Open5e Reference: equipment + magic items tabs
- src/lib/open5e/client.ts: searchEquipment, getEquipmentItem,
searchMagicItems, getMagicItem (plus EquipmentItem / MagicItem types)
- src/lib/client/open5e-ui.ts: Tab type extended to four, search() and
selectItem() dispatch all four
- src/components/dm/Open5eReference.astro: two more tab buttons; new
renderEquipmentCard/Detail and renderMagicItemCard/Detail; filter
dropdown hidden for new tabs (search-by-name only)
- src/lib/client/translation.ts: TranslationType union now includes
"equipment" | "magicitem"
- src/lib/ai/openrouter.ts: translateOpen5eContent type widened (export
Open5eContentType)
- src/pages/api/dm/translate.ts: ALLOWED_TYPES adds equipment/magicitem
and dispatches to the right Open5e fetcher
UX polish
- src/components/dm/AiQuotaExhausted.astro: dedicated state shown to
authenticated FREE users with remaining=0 — replaces the form with
reset-time message and Boosty upgrade hint
- src/pages/{dm,ru/dm}/index.astro: render AiQuotaExhausted when the
guard passes; otherwise render the kind switcher
- src/middleware/index.ts: remove [Middleware] debug console.logs that
were noisy in prod logs (one line per request)
Verified locally: tsc 0 errors, lint 0 errors, vitest 339/339 passing,
npm run build succeeds without auth env vars.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The deploy pipeline was building the app image with only `dist/` inside
and rsync'ing only `dist package*.json docker-compose.yml Dockerfile`
to the server. The drizzle migrations folder never reached prod, so
new schema changes (Wave 1 added `tier`, `boosty_verified_at`, and 5
tables) silently went missing. App requests then failed with
"column does not exist" errors at runtime.
Wave-1 CI step `npm run db:migrate` ran against the in-job postgres
service, not against prod — so it never helped.
Changes:
- src/db/migrate.ts -> src/db/migrate.mjs: plain JS so the prod image
can run it via `node` without devDependencies (no tsx needed).
- Dockerfile: COPY drizzle and src/db/migrate.mjs into the image,
prepend `node ./src/db/migrate.mjs &&` to the CMD. Container fails
to start if migrations fail — better than serving with stale schema.
- .github/workflows/deploy.yml: rsync now also sends `drizzle` and
`src` so the build context on the server has what the Dockerfile
COPYs reference.
- package.json: `db:migrate` script switched to `node src/db/migrate.mjs`.
- eslint.config.mjs: enable node globals for the migrator script.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`validateAuthEnv()` was running at module-load time, which fired during
`astro build` while constructing the route manifest — before any actual
request needed the secrets. CI build failed even though build-time code
doesn't use JWT_SECRET / OAuth secrets.
- src/lib/auth/env.ts: wrap authEnv in a Proxy that runs validation
on first property read; cache the validated object after.
- src/lib/auth/jwt.ts: defer `new TextEncoder().encode(...)` of the
secret behind a memoised getSecret() helper.
- src/lib/auth/oauth.ts: same for the JWT secret; vkOAuthConfig and
yandexOAuthConfig switched to getter-based properties so credential
access is also lazy.
- src/lib/auth/auth.test.ts: 3 tests previously asserted "throws at
module load"; updated to assert "throws on first access" — semantic
guarantee (invalid env throws) is preserved.
Runtime fail-fast is intact: any auth route reading authEnv.X will
throw with the same descriptive Zod error if a var is missing. Build
just no longer crashes when secrets aren't in env (e.g. CI deploy
pipeline running tsc/lint/build without secrets).
Verified: npm run build succeeds with no auth env vars set; tsc 0,
lint 0, vitest 339/339.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 6 API/CORS test files mock the Astro APIContext by spreading
`{} as any` to satisfy fields not under test (cookies, redirect,
clientAddress, etc.). Replacing each cast individually would require
either full typed contexts (obscures tests) or 70+ inline disables.
Add one file-level `eslint-disable @typescript-eslint/no-explicit-any`
with a comment explaining the rationale to each affected file.
Production code remains lint-clean. Verified: tsc 0, lint 0,
vitest 339/339.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an opt-in modal that prompts a PRO user with local data to migrate
their localStorage notes and sessionStorage initiative into the cloud.
- src/lib/client/import.ts: pure helpers (hasLocalData, shouldShowImport)
- src/components/dm/ImportModal.astro: modal UI + import orchestration
with conflict handling for existing "DM Notes" record
- src/i18n/dm-translations.ts: 14 new keys for the import flow
- Wired into both EN and RU dm/index.astro
- tests/import-flow.test.ts: 18 unit tests on the helpers
Local storage is preserved as backup; only a "dm-import-asked" flag is
set after the user makes a decision (import or skip).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix auth anti-patterns (JWT, OAuth, logout, middleware, url.origin)
- Extend DB schema with tier, npcs, counters, translations, notes, initiative
- Expand Open5e TypeScript interfaces (Monster, Spell) to full V2
- Build OpenRouter API client for FREE tier
- Build Kimi API client for PRO tier
- Add comprehensive tests for all modules
- Fix typo "combatантов" -> "участников боя" in dm-translations
- Replace hardcoded English "Result", "Rolls:", "Invalid notation"
in DiceRoller with translation keys
- Translate English aria-labels ("Quick dice selectors", "Roll history",
"Combatants") to Russian
- Move InitiativeTracker hardcoded strings ("Итого", "Инициатива",
"Порядок хода", help text) into the translation file
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bump container max-width and right context column at xl/2xl so the
reference and notes panels get noticeably more room on larger monitors.
Mobile and lg layouts are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pages /dm/ and /ru/dm/ were static (prerendered), so middleware
never ran and Astro.locals.user was always null. Adding
prerender = false allows middleware to authenticate the user
before page render.
- Change COOKIE_NAME from 'session' to 'auth_token' in oauth.ts
- Update logout.ts to use Headers.append() and SameSite=Lax
- Fixes mismatch where callback set 'session' but middleware read 'auth_token'
Browser cannot parse multiple cookies from a single Set-Cookie header
joined by comma (RFC 6265). Use Headers.append() to send each cookie
in its own Set-Cookie header. Fixes state/verifier cookie mismatch.