24 Commits

Author SHA1 Message Date
6682810c00 Nginx: no-cache on index.html, long cache on hashed assets
Without this the browser caches index.html, which contains references
to the current build's JS bundle filename. After a redeploy the new
bundle has a different hash, but the browser still loads the old
index.html → requests the old bundle → may 404 or load stale code.
Symptom: Ctrl+Shift+R required after every update to see the story.

index.html: Cache-Control no-cache/no-store/must-revalidate
JS/CSS/font/image assets: max-age=31536000, immutable (safe because
Vite includes a content hash in every filename)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:04:44 -04:00
c6589b7dcf Fix lint infinite loop, popover usability, debounce timing
Infinite loop (lint hammering server every 2s):
- Add applyingLints ref; set true before clearLintMarks() and
  editor.view.dispatch(tr), false after
- onUpdate returns early when applyingLints is true, so applying
  marks no longer reschedules a lint check or triggers a save

Popover disappearing before you can interact:
- Render via createPortal(…, document.body) — completely outside the
  editor DOM, no stacking context or overflow clipping can interfere
- onMouseDown={e => e.preventDefault()} on the popover prevents the
  editor from losing focus before the button click fires
- Removed setLintPopover(null) from the top of runLint — the popover
  was being force-closed on every auto-check cycle

Debounce: 1.5 s → 3 s — checks after a genuine pause, not mid-word

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:00:56 -04:00
e5b9f643e1 Fix TDZ crash: move useEffect hooks to after runLint declaration
useEffect(() => {...}, [runLint]) evaluated [runLint] immediately,
but runLint was declared with const on the next line — temporal dead
zone. Moved both effects to after the useCallback closes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:52:39 -04:00
91071270a4 Auto-check after every word; disable browser spellcheck
- spellCheck={false} on EditorContent — LanguageTool's wavy marks
  take over entirely, no double-squiggle confusion
- onUpdate schedules runLint() 1.5 s after the last keystroke via
  lintDebounce ref; typing resets the timer so it only fires when
  the writer pauses (naturally after finishing a word or sentence)
- runLintRef kept in sync with useEffect so the stale onUpdate
  closure always calls the latest runLint
- Timer cleaned up on unmount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:48:47 -04:00
bdafd3f2c3 Self-host LanguageTool in Docker Compose
- Add erikvl87/languagetool service on the internal network
  (port 8010, English only by default — change langsToLoad to add more)
- 256 MB min / 512 MB max heap; healthcheck waits up to ~3 min for
  first startup while Java initialises
- Server LANGUAGETOOL_URL hardwired to http://languagetool:8010/v2 —
  no character limits, no rate limits, fully private
- Remove 40 000-char cap from lint route (not needed self-hosted)
- Bump fetch timeout to 30 s for cold-start requests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:43:43 -04:00
55375d2ff0 Built-in spell & grammar checker (LanguageTool)
Server:
- New POST /api/lint/check proxies to LanguageTool API (public free
  endpoint by default; override with LANGUAGETOOL_URL env var for
  self-hosted instance)
- Returns trimmed match list: message, offset, length, replacements,
  kind (spelling|grammar), ruleId
- Disables cosmetic rules that don't suit creative writing
- 20-second timeout; falls back with 502 on error

Frontend:
- LintMark: transient TipTap Mark extension (never saved to DB —
  stripLintMarks() removes them from getJSON() before onChange fires)
- buildTextMap(): walks ProseMirror doc tree, builds parallel plain-
  text string + position array so LanguageTool char offsets map back
  to exact PM positions even across paragraphs / headings
- clearLintMarks() + runLint(): check → clear old marks → apply new
  marks in one transaction, no flicker
- Click on underlined text → fixed-position popover showing the error
  message + up to 6 one-click replacement buttons + Ignore / ✕
- Applying a replacement triggers an automatic re-check after 600 ms
- lintStatus: idle → checking → done → stale (when user edits)
- ABC toolbar button shows count badge when errors found, ✓ when clean
- Wavy red underline = spelling, wavy blue = grammar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:35:38 -04:00
a8f93582bf Read-aloud: voice picker + speed control, auto-avoids eSpeak
- ⚙ button opens a settings popover above the toolbar with:
  - Voice dropdown: lists all voices the browser exposes, with ☁
    suffix for remote/cloud voices vs local ones
  - Speed slider: 0.5x–2.0x in 0.1 steps, labelled live
- On first load, auto-selects the first English non-eSpeak voice
  (helps Linux/Firefox users who only have eSpeak installed by default
  and haven't yet set a preference)
- Voice URI and rate persist in localStorage (sw-tts-voice, sw-tts-rate)
- Voice and rate applied per-utterance via voiceURIRef / ttsRateRef
  so changes take effect on the next sentence without restarting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:21:15 -04:00
c9126f718d DB resilience: hourly WAL checkpoint + daily rolling 7-day backup
- Hourly PASSIVE WAL checkpoint prevents unbounded WAL growth and
  ensures all writes are merged into the main .db file regularly.
  Previously the WAL was never checkpointed — all data was accumulating
  in stories.db-wal with no protection if that file was lost.
- Daily backup using better-sqlite3 .backup() writes a safe online
  snapshot to data/backups/stories-YYYY-MM-DD.db on startup and
  every 24 h; keeps last 7 days, pruning older ones automatically.
- busy_timeout = 5000 so concurrent requests wait briefly rather
  than failing with SQLITE_BUSY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:15:15 -04:00
c6d1554215 Typewriter mode, SVG alignment icons, highlight readability fix
Typewriter mode (⌨ toolbar button):
- Registers TipTap update/selectionUpdate listeners via editor.on()
- On each change, uses coordsAtPos + window.scrollBy to keep cursor
  at 50% of viewport height — instant, no scroll-lag
- Adds 50vh padding-bottom so the last line can reach screen centre
- Toggles .typewriter class on <html>; cleans up on unmount

Alignment buttons:
- Replaced L/C/R text labels with inline SVG stacked-line icons
  (left, centre, right, justify) — standard text-editor appearance
- Added Justify as a fourth option

Highlight readability:
- Added color: #111 !important to .editor-body .ProseMirror mark
  so highlighted text is always dark-on-light regardless of theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:00:14 -04:00
1d00d86709 Add read-aloud: reads story from cursor to end using Web Speech API
- 🔊 button at far-right of toolbar; toggles to ⏹ while reading
- Extracts plain text from cursor position to end of document via
  editor.state.doc.textBetween — works with cursor or selection
- Splits into sentence chunks to work around Chrome's ~15s utterance
  cutoff bug; each chunk's onend fires the next
- isReadingRef guards the closure so Stop cancels mid-sentence cleanly
- Gentle pulse animation on the active button (reading-pulse keyframe)
- No server required — uses built-in browser speechSynthesis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:55:50 -04:00
48d3533bfc Prompt queue: pre-fetch 5, instant local fallback, emoji source icon
- Refill a queue of 5 AI prompts in the background on page load
- Track in-flight fetches with pendingFetches ref so we never over-request
- fetchPrompt() is now synchronous: pops from queue or instantly returns a
  shuffled built-in (LOCAL_PROMPTS, 12 entries, no-repeat cycling)
- Source badge replaced with a small emoji in the top-right corner of the
  popover: 🤖 for AI prompts, 📚 for built-ins
- Removed promptLoading state and spinner entirely

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:48:31 -04:00
c5b7d7f774 Fix AI prompt URL: use /ollama/v1/chat/completions for Open WebUI
The server at ai.binarygnome.com is Open WebUI, not a bare Ollama
instance. Open WebUI proxies Ollama at /ollama/v1/... rather than /v1/...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:35:11 -04:00
6f12efd3fb Populate .env.example with all configurable variables
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:31:36 -04:00
0cae6c6188 Fix AI prompts: switch to OpenAI-compatible /v1/chat/completions API
The server at ai.binarygnome.com returns 405 on POST /api/generate,
which means it speaks the OpenAI-compatible API rather than the native
Ollama format. Switch to /v1/chat/completions with messages[] payload
and data.choices[0].message.content response parsing.

Also add optional OLLAMA_API_KEY env var for servers that require a
Bearer token.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:27:31 -04:00
a4a982aed7 Fix /api/prompts/test — move before auth middleware so it's accessible without a login
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:25:17 -04:00
37448be5a8 Add notes panel, font picker, sticky toolbar, and prompt improvements
- Notes: per-story rich-text notes panel (right drawer) with TipTap
  editor, image support, autosave, and full CRUD API
- Font picker: 15 Google Fonts selectable from a floating Aa button,
  persisted to localStorage via --font-body CSS variable
- Sticky toolbar: pulled formatting bar out of overflow:hidden wrapper
  so it sticks below the topbar while scrolling
- Prompts: 100 additional built-in prompts (120 total) in a shuffled
  no-repeat queue; pre-fetch on page load so the AI has time to respond;
  timeout raised to 45s; error logging + /api/prompts/test debug endpoint;
  source badge shows whether prompt came from AI or built-in list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:21:26 -04:00
b7baf4fa15 Rename app to Grimoire, add book+crescent moon SVG icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:46 -04:00
88e4441222 Fix mobile topbar horizontal overflow
Wrap button labels in spans so the existing hide-on-mobile rule fires.
Hide all topbar text labels and save status on small screens, leaving
only icons. Add overflow-x: hidden to html as a safety net.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:48:41 -04:00
219fd87e5e Remove leftover pocketbase lib
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:11:41 -04:00
9afb1dd4c5 Switch from email to username, add password change and admin reset
- Login now uses username instead of email
- DB migration renames email -> username on existing databases
- Users can change their own password from the Stories page
- Admin can reset any user's password from the admin panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:11:27 -04:00
635f56e44b Fix service DNS by using explicit named network
network_mode: bridge uses Docker's default bridge which has no
service-name DNS. An explicit named network gets its own resolver
so nginx can resolve "server" by name. Also adds a healthcheck so
app waits until server is actually ready before nginx starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:59:53 -04:00
18d238a602 Fix nginx upstream resolution by removing broken Docker DNS resolver
The resolver 127.0.0.11 directive caused ECONNREFUSED errors in this
network setup. Direct proxy_pass resolves server at startup which is
sufficient since depends_on ensures server is running first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:57:02 -04:00
1f51504de9 Add story writer app with editor, auth, export, and polish features
- React + TipTap editor with formatting toolbar (bold, italic, underline,
  strikethrough, alignment, highlight, scene breaks)
- Custom image node view with resize and alignment controls; server-side
  WebP conversion via sharp
- Express + SQLite backend with JWT auth and admin user management
- Export to PDF, EPUB, and ODT
- Five themes (Midnight, Gothic Night, Enchanted Forest, Aged Manuscript,
  Neon Noir); Lora body font for readability
- Writing streak, daily word goal, milestones, and Ollama writing prompts
- Docker Compose setup for self-hosted deployment behind NPMplus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:47:55 -04:00
8777e30d86 first commit 2026-05-11 11:45:44 -04:00