timka123andClaude Opus 5 f25ccdec38 deploy: production compose overlay, nginx vhost and deploy script
Three pieces so kadence.timka.su can be stood up and updated from the repo
rather than from shell history on the box:

* `docker-compose.prod.yml` — publishes the app on loopback only. The base
  file's `${APP_PORT}:8000` binds every interface, which on this host both
  collides with remnawave (already on 8000) and would serve the API beside
  the proxy without TLS. Compose *appends* merged port lists, so the override
  needs the `!override` tag or the public binding survives. uvicorn also gets
  `--proxy-headers`, without which every request looks like plain HTTP from
  127.0.0.1 and the session cookie loses its `Secure` flag.
* `deploy/nginx/kadence.timka.su.conf` — TLS, HSTS, the forwarded headers the
  cookie logic reads, and a 120s read timeout because one task refresh fans
  out to four third-party APIs.
* `deploy/deploy.sh` — idempotent install-or-update. Generates API_TOKEN and
  the database password once and never rotates them afterwards (that would
  lock out the phone and the desktop app), treats `.env.example`'s
  `change-me` placeholders as unset, and uses `certbot certonly --webroot`
  rather than `--nginx` so the vhost in git stays the truth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-19 14:56:57 +03:00
2026-10-19 11:54:15 +03:00
2026-10-19 11:54:15 +03:00
2026-10-18 18:02:05 +03:00
2026-10-19 09:41:32 +03:00
2026-10-18 18:02:05 +03:00
2026-10-19 09:41:32 +03:00
2026-10-18 18:02:05 +03:00
2026-10-19 09:41:32 +03:00

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, 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.
  • Todoist — every open task, or your own filter query.
  • Google Tasks — every incomplete task, including undated ones.
  • 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.

You cannot tick a task off here. Completion belongs to the tool the task lives in — close it in Jira, Todoist or Google Tasks and the next refresh notices (an item that comes back done, or that a healthy source stops returning at all, is credited to that day). Two places to mark something done is two places to be wrong.

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 — and nothing is ever counted as closed on a source that failed. 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.
  • Masks — youtube.com blocks the site plus www. and m.; *.google.com blocks every subdomain in your expansion list (a hosts file has no wildcards, so the app expands them for you and tells you when a pattern like *gram* cannot be expressed that way). Exceptions take masks too, so *.google.com blocked plus *.docs.google.com allowed does what you would expect.
  • Applications — /proc is watched and blocked apps are closed (or just reported, if you prefer a warning). Entries here are globs as well: steam*, *discord*, matched against the process name and its command line.
  • 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

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:

# 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

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
AUTH_USERNAME, AUTH_PASSWORD closes the browser UI behind a login; see Going public
TELEGRAM_OWNER_ID, TELEGRAM_BOT_USERNAME lets that same browser login happen via the Telegram Login Widget

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. 63 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).
  • The look — warm paper, ink type, hairline rules instead of boxes and shadows, Fraunces for figures and headings, Inter for everything else, one ember accent. The desktop app's Theme singleton carries the same tokens as the web app's tokens.ts, so the two clients are one product. Chart colours come from a separate, validated data palette: the brand is warm, but a chart has to stay readable for colour-blind readers in both themes.
  • desktop/ — the native app. See 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:

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
python -m app.cli set-password  # set/rotate the web login password (prompts, never argv)

Going public

Kadence is still single-user: AUTH_USERNAME/AUTH_PASSWORD and TELEGRAM_OWNER_ID all name you, not a user table. What they do is close the browser UI to everyone else once the backend is reachable beyond your own machine — API_TOKEN already protected the raw API; this is the same idea with a login screen instead of a header the desktop app fills in for you.

  1. Password. Set AUTH_PASSWORD (and optionally AUTH_USERNAME, default timka) in .env and restart — the hash is seeded into the database on first boot, the plaintext is never stored. A browser now has to sign in at /; the desktop app is unaffected, it keeps sending X-API-Token. Rotate the password later from Settings → Access (while signed in) or by running make set-password on the server — the UI never accepts a new password over an unauthenticated connection.
  2. Telegram login, in addition (optional). You already have the bot token (TELEGRAM_BOT_TOKEN) for notifications; login reuses it:
    • DM @userinfobot for your numeric Telegram id → TELEGRAM_OWNER_ID.
    • Tell @BotFather /setdomain and give it this site's HTTPS domain — the Login Widget refuses to render anywhere else.
    • Set TELEGRAM_BOT_USERNAME (no @) in .env, or fill it in under Settings → Access once the UI is reachable.
    • Restart. The login screen grows a "Log in with Telegram" button under the password form.
  3. Either credential alone is enough to close the UI; set both and either one signs you in. With neither set, the browser stays as open as the bare API always was — nothing changes until you opt in.

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. The password is never stored at all — only a salted PBKDF2 hash.
  • Set API_TOKEN before exposing the backend beyond localhost (protects the raw API — desktop app, scripts, cron); see Going public to close the browser UI the same way, with a login instead of a header.
  • The login cookie is HttpOnly, SameSite=Lax, and Secure whenever the request arrives as HTTPS (directly or via X-Forwarded-Proto from a reverse proxy) — put the backend behind TLS before relying on it over the open internet, same as API_TOKEN always needed.
  • Login attempts are rate-limited per source IP (in-process; fine for one instance, one user).
S
Description
No description provided
Readme
2.2 MiB
0 Stars 1 Watchers 0 Forks
Languages
Kotlin 51.4%
Python 32.5%
TypeScript 12.3%
QML 2.3%
Shell 1%
Other 0.4%