freezmo/.env.example
Martin Dvorak 77819b5961 Push reminders: personal to the notes, generic without them
The daily reminder said one generic line to everyone, because the server
never sees a deck. Now the notification text is written in the browser
(lib/reminder.ts) and travels with the subscription:

- Personal: "3 cards are due in Sky deck", in the app's language, tapping
  through to /review. Without notes (or with nothing due) it is the plain
  nudge, translated in both languages — push.title / push.due.* / push.generic.
- POST /api/push accepts an optional message {title, body, url}; url must be
  a path on this app, so a stolen subscription cannot make the notification
  link elsewhere. The store keeps endpoint, keys, the message and createdAt
  (a refresh no longer resets the timestamp).
- components/PushReminderSync.tsx refreshes the stored text when the app
  opens and whenever the tab becomes visible (throttled to twice a day), so a
  count is only ever sent while it is true.
- scripts/send-reminders.mjs sends each device its own text and falls back to
  PUSH_REMINDER_TITLE/_BODY for a subscription that has none
  (scripts/reminder-payload.mjs, unit-checked).
- /api/push/test previews the real reminder with a "Test:" prefix.
- deploy/freezmo-reminders.{service,timer}: the daily send as a systemd user
  timer (18:00 local, Persistent, randomised), documented in the README.
- Settings copy, privacy page (en+cs) and .env.example say exactly what the
  server now holds.
- phase2-test grew the whole path with a faked browser subscription: personal
  text in en and cs, the generic fallback in both, the URL guard, and the
  sender's preference order (81 checks).

Version 0.8.2, SW v104.
2026-10-18 14:34:15 +00:00

93 lines
4.5 KiB
Bash

# ---------------------------------------------------------------------------
# Freezmo - Ollama configuration
# Copy this file to `.env.local` and adjust if needed.
# ---------------------------------------------------------------------------
# WHO PROVIDES THE API KEY -----------------------------------------------------
# user-key (default) Every browser supplies its own key. The key is typed
# into the settings dialog, kept in that browser's local
# storage and sent with each request in the
# `x-ollama-key` header. The server never stores it.
# Point OLLAMA_BASE_URL at https://ollama.com.
# server One key for everyone, read from OLLAMA_API_KEY below
# (single-user installs). Nothing is sent from the client.
# local No key at all: a local daemon signed in with
# `ollama signin` proxies the cloud models.
OLLAMA_AUTH=user-key
# Ollama endpoint. https://ollama.com for cloud, http://localhost:11434 for a
# local daemon. In user-key mode this is the endpoint the browser's key is sent
# to; in server/local mode it is used directly.
OLLAMA_BASE_URL=https://ollama.com
# Only used when OLLAMA_AUTH=server. Never commit a real key.
OLLAMA_API_KEY=
# Model used for the vision/transcription step (reads photos of notes).
# Must be multimodal — gpt-oss and other text-only models cannot be used here.
# Free-tier friendly: gemma4:31b-cloud, kimi-k3:cloud (paid), mistral-large-3 (paid).
VISION_MODEL=gemma4:31b-cloud
# Model used to generate activities, grade short answers and restore the
# language if a transcription came back translated. gpt-oss:120b is free-tier
# friendly, very good at strict JSON and the strongest multilingual model in the
# free plan. NOTE: it is text-only, so it can't be used as VISION_MODEL.
# Free alternatives: nemotron-3-super:cloud, gpt-oss:20b-cloud.
# Paid/credit: glm-5.3-flash:cloud, kimi-k3:cloud.
TEXT_MODEL=gpt-oss:120b-cloud
# CHAT MODELS (optional) -------------------------------------------------------
# The allowlist the chat model picker offers, comma-separated. The server never
# accepts a model name outside this list. The first entry is the default.
# When unset, the configured TEXT_MODEL and VISION_MODEL are used.
# Example: CHAT_MODELS=gemma4:31b-cloud,gpt-oss:120b-cloud
# CHAT_MODELS=
# RATE LIMITS (per client IP, sliding window, in-memory) ----------------------
# Generation decks are expensive, so they are limited tightly by default.
# RATE_LIMIT_WINDOW_MS=300000
# RATE_LIMIT_GENERATE=12
# RATE_LIMIT_GRADE=40
# RATE_LIMIT_HOMEWORK=12
# Homework follow-ups (short rewrites).
# RATE_LIMIT_REFINE=30
# RATE_LIMIT_HEALTH=90
# Encrypted sync (no model calls, but shouldn't be hammered).
# RATE_LIMIT_SYNC=60
# Push subscription changes and test sends.
# RATE_LIMIT_PUSH=30
# Chat turns (one model call each; users send them in bursts).
# RATE_LIMIT_CHAT=60
# PUSH REMINDERS (optional) ----------------------------------------------------
# Web Push reminders only appear in the UI when BOTH VAPID keys are set.
# Generate them once with: npx web-push generate-vapid-keys
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
# VAPID_SUBJECT=mailto:you@example.com
# Where subscriptions are stored (endpoint, keys, and the reminder text the
# browser composed for that device — it may name one of its decks).
# PUSH_DIR=.freezmo/push
# The daily sender is a script you schedule yourself — cron, or the shipped
# systemd timer (deploy/freezmo-reminders.timer):
# node scripts/send-reminders.mjs
# A device's own reminder text wins; these generic lines are the fallback for
# subscriptions that never synced one (same words the app uses without notes).
# PUSH_REMINDER_TITLE=Time to review
# PUSH_REMINDER_BODY=Your decks are waiting — a few due cards keep the streak alive.
# ENCRYPTED SYNC STORAGE -------------------------------------------------------
# Opt-in sync uploads an opaque AES-GCM blob; the server can't read it and
# never sees the passphrase or the API key. One small JSON file per sync ID.
# SYNC_DIR=.freezmo/sync
# Per-blob cap in bytes (larger uploads are refused with HTTP 413).
# SYNC_MAX_BYTES=12582912
# Total cap for the directory in bytes (new blobs refused with HTTP 507).
# SYNC_TOTAL_BYTES=1073741824
# Optional tuning ------------------------------------------------------------
# OLLAMA_TIMEOUT_MS=300000
# OLLAMA_HEALTH_TIMEOUT_MS=8000
# Send `think: false` to thinking models for faster JSON output.
# OLLAMA_THINK=false
# FREEZMO_DEFAULT_ACTIVITY_COUNT=10