234 lines
10 KiB
Markdown
234 lines
10 KiB
Markdown
# 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.
|