Commit Graph
122 Commits
Author SHA1 Message Date
emilandClaude Opus 4.7 815c5c7b04 fix(home): move prerender export inside frontmatter so it stops rendering as text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:30:33 +03:00
emilandClaude Opus 4.7 79e1dc4144 fix(deploy): lock down postgres — no public port + password via .env
Yesterday's prod incident exposed postgres on 0.0.0.0:5432 with the
default 'postgres:postgres' credentials. A scanner ransomware bot
brute-forced it and dropped the database (left a readme_to_recover
note). We restored from a pre-incident dump and the user data is back,
but the underlying weakness was in this docker-compose.yml.

Changes:
- Remove `ports: "5432:5432"` from the postgres service entirely.
  Postgres is reachable only via the internal docker network. For
  ad-hoc admin access, use an SSH tunnel:
  `ssh -L 5432:localhost:5432 deploy@<host>`
- POSTGRES_PASSWORD now reads from `${POSTGRES_PASSWORD:-postgres}`
  via env interpolation. Prod `.env` (not in repo) provides the real
  value; local dev gets the `postgres` fallback so `docker compose up`
  still works without setup.
- Remove the no-longer-needed DATABASE_URL override in the app service
  `environment:` — `env_file: .env` already supplies it.

After this lands, the deploy pipeline will rsync the new compose.yml,
recreate containers with no exposed pg port, and substitute the strong
password from .env at container start. Volumes persist, data intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 02:56:53 +03:00
emilandClaude Opus 4.7 cce582df6c feat(dm-dashboard): Wave 8 — spell generator + Open5e equipment/magicitems + UX polish
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>
2026-05-16 02:47:41 +03:00
emilandClaude Opus 4.7 e6dec1eb95 fix(deploy): apply drizzle migrations to prod on container startup
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>
2026-05-16 00:29:32 +03:00
emilandClaude Opus 4.7 4b78e9edad fix(auth): defer env validation to first access so build works without secrets
`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>
2026-05-15 23:45:03 +03:00
emilandClaude Opus 4.7 f2afaf60a3 fix(lint): unblock CI — add scoped eslint-disable for APIContext mocks
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>
2026-05-15 23:39:50 +03:00
emilandClaude Opus 4.7 2f7453e571 chore(qa): Final Wave cleanup — resolve lint findings from F2 review
Production code (all findings cleared):
- GenerationHistory: drop unused fetchProHistory(userId) param,
  change let activeId to const
- InitiativeTracker: remove write-only currentSessionName state
- NotesPanel: remove unused isLoading flag
- Open5eReference: drop unused getCachedTranslation/setCachedTranslation
  imports; use fetchTranslation result directly instead of re-lookup
- ai/openrouter.ts: drop unused z + Monster imports; attach cause to
  rethrown AbortError
- ai/kimi.ts: drop unused z import
- api/dm/ai/generate.ts: annotate best-effort counter increment catch
- api/dm/ai/history.ts: drop unused sql import

Test files (trivial fixes; ~70 mock-related `as any` errors remain as
pre-existing tech debt across api test mocks):
- translation-client: drop unused MockBroadcastChannel _name param and
  unused getCachedTranslation destructure
- open5e-types: eslint-disable for the intentional type-test aliases
- {notes,initiative}-api: remove unused mockUser2 placeholder
- {notes,initiative,translate,history}-api: let mockDb -> const mockDb
- ai-generate-api: wrap deliberate `var` mock decls in eslint-disable
  with comment explaining vi.mock hoisting requirement

Verification: tsc 0 errors, vitest 339/339, lint 81 -> 70 errors
(production code fully clean; remaining errors are test-mock as-any
patterns from Waves 5-6).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 23:33:57 +03:00
emilandClaude Opus 4.7 701398f627 feat(dm-dashboard): Wave 7 — FREE→PRO data import flow (Task 22)
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>
2026-05-15 23:33:39 +03:00
emil 09e947f430 feat(dm-dashboard): Wave 6 — NPC form, result card, history panel, notes/initiative DB migration, translation service 2026-05-15 23:09:06 +03:00
emil 75f2dc6972 feat(dm-dashboard): Wave 5 — NPC generation API, history API, quota/tier badges, translate button 2026-05-15 22:45:26 +03:00
emil 2921e45237 feat(dm-dashboard): Wave 4 — rate limiting, translation API, AI tab navigation 2026-05-15 22:26:41 +03:00
emil 5aec915034 feat(dm-dashboard): Wave 3 — Boosty verification, notes/initiative API routes 2026-05-15 22:00:19 +03:00
emil 0bbaa6e3d8 feat(dm-dashboard): Wave 2 — Drizzle migration, DmSidebar fix, CORS config 2026-05-15 21:48:09 +03:00
emil 25f43ec4e2 feat(dm-dashboard): Wave 1 — auth fixes, DB schema, Open5e interfaces, AI clients
- 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
2026-05-15 21:28:42 +03:00
emilandClaude Opus 4.7 1f09270329 fix(dm): polish hardcoded strings, typo, and aria-labels
- 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>
2026-05-15 18:31:21 +03:00
emilandClaude Opus 4.7 c01c34d3e0 style(dm): widen desktop dashboard at xl/2xl breakpoints
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>
2026-05-15 18:23:35 +03:00
emilandSisyphus 659b042de5 fix(lint): resolve explicit any and empty catch in DM test scripts
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 18:10:36 +03:00
emilandSisyphus 8eafd504b2 fix(lint): resolve explicit any and useless assignment in DM components
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 18:10:28 +03:00
emilandSisyphus 3388d70d96 chore(lint): add debug-open5e.mjs to ESLint ignores
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 18:10:19 +03:00
emilandSisyphus fddc40e967 docs: note boulder tracking sync issue in DM dashboard redesign
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 18:02:00 +03:00
emil 1c38df7e74 style(dm): redesign dashboard with purple-gold theme
Redesign DM Dashboard (/dm/, /ru/dm/) with purple-gold theme (#534AB7 + #c8a84b):

Design System:
- Update dm-theme.css with 49 purple-gold tokens (transitions, shadows, hover states)
- Create DmTabs.astro with ARIA, keyboard nav, URL hash sync, localStorage persistence
- Create DmSidebar.astro for desktop navigation
- Redesign DmHeader with gold accent
- Update DmButton, DmCard, DmInput primitives
- Expand i18n to 47 strings

Layout:
- Redesign DmLayout with 3-column dashboard grid (sidebar|main|context)
- Update /dm/index.astro and /ru/dm/index.astro with named slots

Feature Components:
- DiceRoller: empty state, ARIA live, color-coded history, storage key fallback
- InitiativeTracker: HP editing, active highlighting, storage key fallback
- Open5eReference: skeletons, debounce, error retry, gold tabs
- NotesPanel: cross-tab sync, copy-to-clipboard, auto-save
- AuthPanel: VK/Yandex brand colors, pill buttons

Tests:
- Add 50 Playwright E2E tests (smoke, dice, initiative, reference, notes, data-migration)

Fixes:
- Document Open5e client.ts URL bugfix
- Add backward-compatible storage key fallback for dice/initiative
- Preserve legacy sessionStorage data on load
2026-05-15 17:49:32 +03:00
emil 11da171bfb docs: add AGENTS.md knowledge base (root + generators + dm) 2026-05-15 04:35:00 +03:00
emil 592fa4cc3e chore(vk): remove debug logging from callback 2026-05-15 04:21:34 +03:00
emil 5815b52c4e fix(vk): add client_id to user_info request, use query params for device_id 2026-05-15 04:18:19 +03:00
emil 57d56c7f29 debug(vk): log full callback URL and all query params 2026-05-15 04:15:07 +03:00
emil 258f062115 fix(lint): remove unused refreshToken variable 2026-05-15 04:11:07 +03:00
emil fbc011f55e fix(vk): use id.vk.ru domain, handle payload param, add device_id to token exchange 2026-05-15 04:09:02 +03:00
emil 5b6247ff7c style(auth): remove debug border from AuthPanel, clean up console.log 2026-05-15 03:55:51 +03:00
emil 6f2d9aff5d fix(auth): make DM pages server-rendered so middleware can set Astro.locals.user
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.
2026-05-15 03:50:41 +03:00
emil 38a778f3de debug(auth): add visible debug border and user state to AuthPanel 2026-05-15 03:40:01 +03:00
emil 08161dcdd1 fix(auth): unify cookie name to auth_token for OAuth and middleware compatibility
- 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'
2026-05-15 02:22:19 +03:00
emil de81e54ae1 fix(oauth): use SameSite=Lax for session cookie to survive cross-site redirect 2026-05-15 02:14:21 +03:00
emil b762e2eb66 fix(health): check DB connectivity in health endpoint 2026-05-15 02:05:40 +03:00
emil b71902b686 fix(oauth): use separate Set-Cookie headers for multiple cookies
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.
2026-05-15 01:59:56 +03:00
emil da6208de50 fix(oauth): fix VK token URL (.ru -> .com), add error logging to callbacks 2026-05-15 01:50:45 +03:00
emil da0ee46bc0 fix(dm): scope DM theme CSS to .dm-theme class to override BaseLayout purple accent 2026-05-15 01:46:01 +03:00
emil aaf596a7bc fix(dm): OAuth redirect_uri, orange buttons, denser layout, hide lang switcher
- Fix OAuth redirect_uri mismatch in VK/Yandex callbacks (use PUBLIC_APP_URL)
- Make quick dice buttons and anchor nav links orange by default
- Densify DM Dashboard layout (smaller gaps, tighter spacing)
- Hide LanguageSwitcher/Blog/About/Privacy links on DM pages
- Increase notes textarea height for better usability
2026-05-15 01:42:09 +03:00
emil 5273518908 fix: SameSite=Lax for OAuth cookies to allow cross-site redirects 2026-05-15 01:28:24 +03:00
emil 2b57cba3bb fix: use process.env for runtime OAuth secrets 2026-05-15 01:25:27 +03:00
emil 4a13ba3272 fix deploy: copy dist directory instead of contents 2026-05-15 01:01:55 +03:00
emil d24f978d7e trigger deploy 2026-05-15 00:54:45 +03:00
emil 1a092e05c1 feat(dm): add dashboard with oauth, dice, initiative, open5e, notes 2026-05-14 23:48:36 +03:00
emil 7efe188a9a feat(ads): restore custom AdBanner with Admitad link 2026-05-14 16:21:50 +03:00
emil 87b35ba067 fix(ads): remove Admitad and Chitai-gorod banners completely 2026-05-14 15:55:37 +03:00
emil dfca40bb34 fix(lint): remove unused Props and eslint-disable directive 2026-05-14 15:38:04 +03:00
emil b22a68bf12 feat(ads): replace custom banner with Admitad leaderboard 2026-05-14 15:33:48 +03:00
emil 375455aac9 fix(ads): remove Yandex branding from banner, use neutral accent styling 2026-05-14 15:11:50 +03:00
emil 7c4241fd0e feat(ads): remove Yandex ads, move banner above generators 2026-05-14 15:07:17 +03:00
emil ae809cd119 feat(ads): replace Yandex Market with Admitad Читай-город affiliate link 2026-05-14 15:00:54 +03:00
emil f6f1ca05c9 feat(seo): add Admitad verification file 2026-05-14 14:48:07 +03:00