Files
kadence/README.md
T
2026-10-18 18:02:05 +03:00

234 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kadence
**Find the rhythm of your work day.** A pomodoro timer that knows what you have
to do, stands between you and the distractions, and tells you in Telegram how
the day went.
Three pieces, one backend:
| Piece | What it is | Where it runs |
| --- | --- | --- |
| **Backend** | FastAPI + Postgres: the clock, the task mirror, the blocking policy, the reports, the Telegram bot | Docker |
| **Web app** | React + Material UI — the whole thing in a browser, on any device | Docker (same container) |
| **Desktop app** | A **native Qt/QML** app: tray, D-Bus, real notifications, and the part that actually blocks websites and apps | Your machine |
The server owns the clock. Start a pomodoro on your phone's browser, watch it
count down in the tray, finish it from the desktop — it is one interval.
---
## What it does
### A fully configurable pomodoro timer
Focus / short break / long break lengths, how many cycles before a long break,
auto-start of breaks and of the next interval, a daily goal, pausing (or not),
strict mode (abandoning a focus interval costs you a written reason),
interruption counting, alarm sound and volume. Intervals are recorded as they
really happened — planned vs actual, paused time, interruptions — so the stats
are not a guess.
### Every todo you own, in one list
Sourced from the integrations worked out in [`~/code/jira-helper`](../jira-helper),
and taking **all** of them:
* **Jira** — every open issue across the site (not just what is due today), with
the due date, the "Start date" custom field, the status, the priority, the
project. Finishing a Jira-backed task here **transitions the issue to Done**.
* **Todoist** — every open task, or your own filter query. Finishing closes it in
Todoist.
* **Google Tasks** — every incomplete task, including undated ones. Finishing
marks it completed in Google.
* **Laundry bookings** (`4ka.inbicst.ru`) — the slots you booked today, because
they are blocks of time that will interrupt your pomodoros.
* **Local tasks** — the "and this too" you type at 9am.
Every source is pulled independently: an expired laundry cookie or a Jira outage
costs you that one source and shows up as a warning next to it, never as an
empty list. The list is mirrored into the database on every refresh, so the UI is
instant and the daily report still works when a source is down.
### A focus guard that actually blocks
Rules live on the server (edit them in the web app or the desktop app); the
native desktop app enforces them on your machine:
* **Websites** — `/etc/hosts` is rewritten through a tiny privileged helper under
polkit. No browser extension, no proxy, works for every app on the machine, and
you can audit it with `cat /etc/hosts`.
* **Applications** — `/proc` is watched and blocked apps are closed (or just
reported, if you prefer a warning).
* **When** — only while a focus interval runs (the default), always, or on a
schedule. Breaks can keep the block on or let it go.
* Every attempt that gets turned away is recorded — and teased about in the
evening report.
### A daily report that is worth reading
Pomodoros vs goal, focus and break minutes, the longest unbroken stretch, when
you started and stopped, interruptions, abandoned intervals, tasks closed (with
links), the sites you tried to sneak off to, your streak, a comparison against
your 7-day average, **achievements** (🎯 goal met, 🧘 deep work, 🦉 night owl,
🪃 comeback, 🚀 personal best…) and **fun facts**:
```
🍅 Daily report — 2026-10-18
████████░░ 6/8 pomodoros
⏱ Focus: 2h 35m · breaks: 35m
🧠 longest stretch: 1h 20m · 🕘 from 09:12 to 18:40 · 📵 interruptions: 3
🔥 Streak: 4 days (best 11)
📊 7-day average: 5.4 · yesterday: 7
✅ Tasks closed (3)
• PLAT-184 Fix the flaky importer (Jira)
• Write the design doc (Todoist)
…
🚫 Temptations blocked
• youtube.com × 12
• reddit.com × 4
🏅 Achievements
🧘 Deep work — 80 minutes without breaking the chain.
🛡️ Gatekeeper — The blocker turned you away 16 times. It won.
🍅 155 minutes of focus — that is 7.0 sitcom episodes you did not watch.
⏰ Your peak hour was 10:00. Defend it.
Target hit. Now protect the streak. 💪
```
Four tones (friendly / coach / drill sergeant / dry) and two languages (English,
Russian).
### A Telegram bot that pushes back
Unlike jira-helper's one-way daily message, this one is a remote control:
* **Morning briefing** — your day, grouped: overdue, due today, coming up, plus
your streak and the goal to beat.
* **Daily report** at your hour, **weekly review** on your day.
* **Nudges** — "90 minutes without a pomodoro. You are at 2/8 — one block puts you
back on plan." Windowed, rate-limited, and silent during quiet hours.
* **Cheers** every N pomodoros.
* **Commands**: `/start_work [minutes]`, `/break`, `/pause`, `/resume`, `/stop`,
`/status`, `/today`, `/done 3`, `/report`, `/week`, `/streak`, `/block`.
---
## Quick start
```bash
git clone <this repo> kadence && cd kadence
cp .env.example .env # fill in Jira / Todoist / Telegram (optional)
docker compose up -d --build # → http://localhost:8000
```
That is the backend and the web app. Then the native desktop app:
```bash
# Arch: the only dependency is PyQt6, which is a system package
sudo pacman -S --needed python-pyqt6 qt6-declarative
# (Debian/Ubuntu: sudo apt install python3-pyqt6 python3-pyqt6.qtquick qml6-module-qtquick-controls)
./desktop/packaging/install-desktop.sh # menu entry + icon + `kadence-desktop`
sudo ./desktop/helper/install.sh # the privileged helper, for website blocking
kadence-desktop
```
Point it at the backend in **Settings → Backend** (it defaults to
`http://localhost:8000`).
### Without Docker
```bash
make backend-venv # backend/.venv with the dependencies
cd backend && .venv/bin/python -m app.cli serve --reload
cd frontend && npm install && npm run dev # http://localhost:5173
```
---
## Configuration
Two layers, on purpose:
* **Environment** (`.env`) — infrastructure and **secrets**: the database, the
API token, and the seeds for the integrations. The variable names are
jira-helper's, so you can copy that `.env` across unchanged.
* **Settings** (the UI) — everything else, stored as one validated document in
the database and shared by the web app, the desktop app and the bot. Secrets
are masked (`••••••••`) on the way out and never clobbered on the way back.
Nothing has to be in the environment: a fresh install is a working 25/5/15 timer,
and you can fill everything in from **Settings**.
| Variable | What for |
| --- | --- |
| `DATABASE_URL` | set by compose; SQLite works too (`sqlite:///./kadence.db`) |
| `API_TOKEN` | if set, every `/api` call must send `X-API-Token`. Set it the moment the backend leaves localhost |
| `TIMEZONE` | which day a pomodoro belongs to, and when the jobs fire |
| `JIRA_*`, `TODOIST_*`, `GOOGLE_*`, `LAUNDRY_*` | seeds for the sources |
| `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` | the bot |
| `SCHEDULER_ENABLED`, `TELEGRAM_BOT_ENABLED` | switch the background jobs / bot off |
---
## How it fits together
```
┌──────────────┐ ┌───────────────────────────────┐ ┌───────────────┐
│ Web app │ │ Backend (FastAPI) │ │ Jira │
│ React + MUI │──/api──│ • timer engine (owns clock) │───────▶│ Todoist │
└──────────────┘ │ • task mirror + write-back │ │ Google Tasks │
│ • blocking policy │ │ Laundry │
┌──────────────┐ │ • reports + achievements │ └───────────────┘
│ Desktop app │──/api──│ • scheduler (briefing, │
│ Qt/QML, D-Bus│ │ report, nudges) │───────▶ Telegram ⇄ you
│ hosts + /proc│ └───────────────────────────────┘
└──────────────┘ │
enforces Postgres
```
* `backend/` — FastAPI app. `app/services/` holds the thinking (timer, tasks,
stats, report, blocker policy, scheduler, bot); `app/sources/` holds one module
per integration; `app/api/` is a thin HTTP layer. 44 tests under `backend/tests`.
* `frontend/` — React + TypeScript + MUI, built by Vite into the backend image and
served from the same origin (so there is no CORS to configure).
* `desktop/` — the native app. See [`desktop/README.md`](desktop/README.md).
---
## Make targets
```
make up # docker compose up --build
make dev # backend with reload + Vite dev server
make test # backend test suite
make desktop # run the native app from the checkout
make desktop-install # menu entry + icon
make helper-install # the privileged blocking helper
make report # build and send today's report right now
```
The backend also has a CLI, handy from cron or a systemd timer:
```bash
python -m app.cli refresh # pull every source once
python -m app.cli briefing # send the morning briefing
python -m app.cli report # build and send today's report
python -m app.cli stats # print the numbers as JSON
```
---
## Security notes
* The desktop app never runs as root. The only privileged code is
`desktop/helper/kadence-helper` (~200 lines): it validates every hostname,
rewrites only the region between its own markers in `/etc/hosts`, writes
atomically, keeps a backup, and refuses anything that does not look like a
hostname. It is authorised by polkit (`auth_admin_keep` by default — one prompt
per session; `install.sh --passwordless` drops that for one user).
* Tokens live in the database and in `.env`, are masked in every API response,
and are never sent to the browser in full.
* Set `API_TOKEN` before exposing the backend beyond localhost; put it behind TLS
if it leaves your network.