Commit Graph
4 Commits
Author SHA1 Message Date
lukasandClaude Opus 5.5 1f2e539eb2 Compose: spelling and grammar check with LanguageTool
A self-hosted LanguageTool (dev: docker/docker-compose.yml, started by
`make up`) checks what the user writes in the compose window, through
POST /api/spellcheck — the browser never talks to the engine directly.

- Marks are CSS highlights over the text, never written into the message: red
  wavy for spelling, blue for grammar/style. Clicking one shows the message,
  up to five suggestions (applied as typed, so Ctrl+Z undoes them) and Ignore.
  The popup, like the link popup, opens above its anchor when there is no
  room below.
- Checked after a pause in typing; the signature and quoted original are
  skipped; marks on words just edited drop at once; a check overtaken by a
  newer one, by turning checking off, or by closing the window is discarded.
- With "auto", text is checked in chunks of lines, each in its own detected
  language — LanguageTool settles on one language per request, so a mixed
  Polish/English message had every English word flagged as a Polish typo.
- Settings > Appearance: on/off and language (Auto-detect or any of the
  engine's languages); the compose window's More menu overrides it for one
  message. The browser's own spellcheck steps aside while ours runs, and comes
  back when it is off or the server has no engine (503 from the endpoint).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:51:31 +02:00
lukasandClaude Opus 5 5553b5febe Deploy: production Docker stack for a single host (Synology/QNAP NAS)
Everything needed to run the app on an Intel NAS that is also doing other
jobs: one published port, no host networking, memory/CPU caps and log
rotation on every container, and all state in bind mounts under DATA_ROOT
so mail survives image updates, project deletion in the NAS UI, and full
reinstalls — and so Hyper Backup/HBS3 can actually see it.

- docker/Dockerfile.backend: multi-stage, non-root (uid 10001), tini for
  the <10s graceful lease release. Installs the four plugins as entry-point
  distributions. One image, several roles via docker/entrypoint.sh
  (api | worker-sync | worker-jobs | migrate | reindex | plugins).
- docker/Dockerfile.frontend + nginx.conf: SPA + /api reverse proxy with
  the WebSocket upgrade for /api/ws, and X-Forwarded-Proto preserved so
  the API still emits __Host-/Secure cookies when TLS is terminated by
  the NAS reverse proxy (identity/cookies.py keys off request scheme).
- docker/docker-compose.prod.yml: project name webmail-prod, NOT webmail —
  docker-compose.yml (dev services) owns that name, and two compose files
  sharing one project name fight over each other's containers.
  Meilisearch is an opt-in profile; SEARCH_BACKEND=pg is the default so a
  small NAS pays nothing for search.
- migrate runs as a one-shot (core Alembic head + every plugin branch)
  that everything else waits on, so a bad migration can never leave new
  code running against an old schema.
- docker/backup.sh: nightly pg_dump verified with `pg_restore --list`
  before it counts as a backup, with retention and a baseline on first
  start.
- deploy/webmail.sh: init/doctor/up/down/status/logs/update/rollback/
  backup/restore/reindex/psql/shell. `update` is backup -> images ->
  migrate -> roll workers, api, web -> health-gate, with automatic
  rollback to the previous version if the gate fails. `doctor` refuses to
  be quiet when DATA_ROOT is on NFS/SMB (silent Postgres corruption).
- deploy/build-push.sh: build images off-NAS (the frontend build wants
  ~2GB RAM) and push to Gitea, or --save a tar to carry over.
- scripts/reindex_search.py: rebuild the search index from Postgres alone.
  Every SearchDoc field is already stored, so the index is derived data
  that never needs backing up — this closes the only real objection to
  running Meilisearch on a box whose volumes may be wiped.
- pyproject: package-data for accounts/presets.yaml and the shared plugin
  Alembic env. Without it a non-editable install (i.e. any image) silently
  ships a package that fails at runtime.
- GET /api/version, used by the deploy health gate.

Verified end to end on a real stack: fresh install, all services healthy,
core + 4 plugin migration branches applied, login/SPA/plugin bundles
through nginx, WS upgrade returning 101, __Host- cookies under a
simulated TLS proxy, backup -> restore round-trip preserving user and
plugin schema, failed-update auto-rollback (both a bad migration and a
boot-crashing API), and Meilisearch profile serving a live query.
ruff + mypy --strict clean, 363/363 backend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
2026-09-14 18:53:50 +02:00
lukasandClaude Opus 4.8 e0a45c5a46 sync-v2 step 1: worker roles + interactive/bulk queue split
- config: WORKER_ROLE (sync|jobs|all), INTERACTIVE_CONCURRENCY, BULK_CONCURRENCY.
- queue: named queues (interactive/bulk) + enqueue_job() router; sync_bootstrap
  goes to bulk, all other jobs to interactive.
- workers.run: single entrypoint; role=jobs runs both arq consumers (per-queue
  timeout/concurrency), role=sync runs the supervisor, all=both (dev default).
- docker-compose: worker-sync + worker-jobs services (shared image, nofile
  ulimit); backend/Dockerfile; Makefile worker/worker-sync/worker-jobs targets.
- tests repointed to app.queue.get_arq_pool; supervisor behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 19:53:31 +02:00
lukasandClaude Opus 4.8 a5ec0d21cb Initial commit: webmail client (M0/M1)
Three-pane webmail client.
- Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP
  sync, threading, search, drafts/outbox with undo, attachments, 2FA.
- Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane
  UI with rich-text composer, light/dark themes, resizable panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:20:34 +02:00