8.5 KiB
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 —
.envfor 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 <duration> |
/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 <message> |
Free-form coaching conversation |
Setup
| Command | Action |
|---|---|
/setup |
First-run onboarding: current level, typical availability, goal — generates initial schedule |
/settings model <id> |
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 <id>.
Alternative models (all via OpenRouter):
deepseek/deepseek-chat— very cheap, good for analytical tasksmeta-llama/llama-3.3-70b-instruct— free on some providersopenai/gpt-4o-mini— reliable, cheap, structured-prompt-friendlyqwen/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:
[
{ "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:
builderstage: installs all dependencies, compiles TypeScript todist/runnerstage: copies onlydist/and productionnode_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-stoppedso 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:
settingsandscheduletables can gain auser_idcolumn - Export: Sessions table trivially exportable to CSV or JSON