chore: project scaffolding
This commit is contained in:
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 (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
|
||||
Reference in New Issue
Block a user