chore: project scaffolding

This commit is contained in:
2026-05-20 17:21:54 +02:00
commit 9c2973faf5
9 changed files with 8266 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,235 @@
# 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 <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 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 (714 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