# Training Assistant — Design Spec **Date:** 2026-05-20 **Status:** Approved --- ## Overview A personal Telegram bot that tracks anal training sessions, sends reminders, and uses an AI coach (via OpenRouter) to adaptively manage the training schedule and provide progress feedback. Built with Node.js/TypeScript, Telegraf, SQLite, and node-cron. Designed so a web dashboard (Approach C) can be added later as a read-only layer on the same SQLite file. --- ## Architecture A single TypeScript process with four internal components: ``` ┌─────────────────────────────────────────┐ │ Telegram Bot (Telegraf) │ │ - command routing │ │ - message handling │ └──────────────┬──────────────────────────┘ │ ┌───────┼───────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────┐ ┌─────────────┐ │ Session │ │ Cron │ │ AI Coach │ │ Logger │ │ Jobs │ │ (OpenRouter)│ └────┬─────┘ └──┬───┘ └──────┬──────┘ │ │ │ └──────────┴─────────────┘ │ ┌──────▼──────┐ │ SQLite │ └─────────────┘ ``` **Components:** - **Bot handler** — Telegraf v4, routes commands and free-text messages - **Session logger** — writes training sessions to SQLite, validates input - **Cron jobs** — `node-cron`, sends reminders at configured times - **AI coach** — OpenRouter via OpenAI-compatible client; reads history from DB, generates coaching and schedule proposals - **SQLite** — `better-sqlite3`, single file, easy to back up - **Config** — `.env` for Telegram token, OpenRouter API key, user chat ID, reminder times, preferred model --- ## Data Model ### `sessions` | Column | Type | Notes | |---|---|---| | `id` | INTEGER PRIMARY KEY | | | `date` | TEXT | YYYY-MM-DD | | `duration_min` | INTEGER | | | `notes` | TEXT | Optional free text | | `created_at` | TEXT | ISO timestamp | ### `schedule` | Column | Type | Notes | |---|---|---| | `id` | INTEGER PRIMARY KEY | | | `date` | TEXT | YYYY-MM-DD | | `planned_min` | INTEGER | | | `status` | TEXT | `pending` \| `completed` \| `skipped` | | `skip_reason` | TEXT | Optional | | `ai_rationale` | TEXT | Why the AI scheduled this | | `created_at` | TEXT | ISO timestamp | ### `settings` | Column | Type | Notes | |---|---|---| | `key` | TEXT PRIMARY KEY | | | `value` | TEXT | | Used for: reminder times, preferred OpenRouter model, training goal, reminder enabled flag. **Session logging** writes to `sessions` and updates the matching `schedule` row to `completed`. The AI reads both tables to understand history and the current plan. The existing `.ics` calendar is not imported. The AI generates a fresh adaptive schedule on first run, seeded by onboarding answers. --- ## Commands ### Logging (no AI, always fast) | Command | Example | Action | |---|---|---| | `/log ` | `/log 2h`, `/log 90m` | Record a completed session for today | | `/skip [reason]` | `/skip work trip` | Mark today as skipped | | `/snooze` | `/snooze` | Postpone today's reminder by 2 hours | ### Status & history | Command | Action | |---|---| | `/status` | Today's plan, completion state, next reminder time | | `/history` | Last 7 sessions (date, duration, notes) | | `/streak` | Current streak and longest streak | ### AI-powered | Command | Action | |---|---| | `/progress` | AI analyzes recent weeks, gives assessment and flags patterns | | `/next` | AI proposes the next training step (duration or frequency change) | | `/reschedule [note]` | AI regenerates upcoming schedule; optional context ("busy next week") | | `/chat ` | Free-form coaching conversation | ### Setup | Command | Action | |---|---| | `/setup` | First-run onboarding: current level, typical availability, goal — generates initial schedule | | `/settings model ` | Switch OpenRouter model at runtime without redeploying | **Fallback:** Unrecognized messages prompt the user to use `/chat` for free-form questions. **Error handling:** AI commands show a clear error message if OpenRouter is unavailable. Logging commands (`/log`, `/skip`) always work regardless of AI availability. --- ## AI Integration **Client:** `openai` npm package pointed at `https://openrouter.ai/api/v1`. Model is read from `settings` at runtime. **Context sent on every AI call:** - Training goal (from setup) - Last 30 sessions (date, duration, notes) - Upcoming schedule with statuses - Recent skips and their reasons - Current streak **Default model:** `google/gemini-2.5-flash-preview` Chosen for strong reasoning, low cost, fast responses, and large context window. Swappable at runtime via `/settings model `. **Alternative models (all via OpenRouter):** - `deepseek/deepseek-chat` — very cheap, good for analytical tasks - `meta-llama/llama-3.3-70b-instruct` — free on some providers - `openai/gpt-4o-mini` — reliable, cheap, structured-prompt-friendly - `qwen/qwen-2.5-72b-instruct` — strong instruction following, low cost **Prompt strategy:** - Coaching prompts live in a `prompts/` directory — tweakable without touching logic - The AI is instructed to be encouraging but honest, explain its reasoning, and flag overtraining or undertraining - Schedule proposals are requested as JSON for reliable parsing: ```json [ { "date": "2026-05-22", "planned_min": 60, "rationale": "Continuing current level" }, { "date": "2026-05-24", "planned_min": 60, "rationale": "Rest day between" }, { "date": "2026-05-26", "planned_min": 75, "rationale": "Step up if 22nd went well" } ] ``` --- ## Reminder & Scheduling Logic ### Cron jobs | Job | Default time | Condition | |---|---|---| | Morning reminder | **05:30** | Session scheduled today and status is `pending` | | Evening nudge | **16:30** | Session scheduled today and status still `pending` | | Snooze | 2 hours after `/snooze` | One-off, clears itself after firing | No reminders fire on days with no scheduled session, or on days already `completed` or `skipped`. ### Adaptive schedule generation `/reschedule` sends full history to the AI, which returns a JSON array of upcoming sessions (7–14 days). The bot parses this into `schedule` rows. Skip reasons are included in the next `/reschedule` call so the AI accounts for gaps. --- ## Project Structure ``` src/ bot/ # Telegraf setup, command handlers db/ # SQLite schema, queries scheduler/ # node-cron jobs ai/ # OpenRouter client, context builder prompts/ # Prompt templates config.ts # Env loading and validation index.ts # Entry point .env.example Dockerfile docker-compose.yml .dockerignore package.json tsconfig.json ``` --- ## Deployment ### Local development Run directly with `npm run dev` (ts-node or tsx). Uses `.env` file for config. ### Production — Docker The app runs as a Docker container on the home server. SQLite data is persisted via a named volume mounted at `/app/data/`. **Dockerfile** — multi-stage build: 1. `builder` stage: installs all dependencies, compiles TypeScript to `dist/` 2. `runner` stage: copies only `dist/` and production `node_modules`, runs as a non-root user **docker-compose.yml** — single service definition: - Mounts a named volume for the SQLite file (`./data:/app/data`) - Loads env vars from `.env` (not baked into the image) - `restart: unless-stopped` so it survives reboots **SQLite file location:** `/app/data/assistant.db` inside the container, mapped to `./data/assistant.db` on the host — easy to back up by copying that file. **`.dockerignore`** excludes: `node_modules/`, `src/`, `.env`, `data/`, `*.md`. **Timezone:** Container timezone set via `TZ` env var (e.g. `TZ=Europe/Amsterdam`) so cron jobs fire at the correct local times. **Updating:** `docker compose pull && docker compose up -d --build` rebuilds from source and restarts with zero downtime for a single-container setup. --- ## Future Extension Points - **Web dashboard (Approach C):** Read-only layer on the same SQLite file — no schema changes needed - **Multi-user:** `settings` and `schedule` tables can gain a `user_id` column - **Export:** Sessions table trivially exportable to CSV or JSON