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.
93 lines
4.5 KiB
Bash
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
|