{"id":51240871,"url":"https://github.com/petronijus/mindbaboon","last_synced_at":"2026-06-29T00:02:41.800Z","repository":{"id":365428825,"uuid":"597855734","full_name":"petronijus/Mindbaboon","owner":"petronijus","description":"Goal tracker with iteration-based email reminders — Flask + SQLite + APScheduler, Google OAuth, REST API + MCP server, packaged as Docker.","archived":false,"fork":false,"pushed_at":"2026-06-17T10:16:39.000Z","size":7532,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-17T10:26:58.727Z","etag":null,"topics":["apscheduler","docker","flask","goal-tracker","mcp","python","self-hosted"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/petronijus.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-02-05T20:41:26.000Z","updated_at":"2026-06-17T10:16:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/petronijus/Mindbaboon","commit_stats":null,"previous_names":["petronijus/mindbaboon"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/petronijus/Mindbaboon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petronijus%2FMindbaboon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petronijus%2FMindbaboon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petronijus%2FMindbaboon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petronijus%2FMindbaboon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/petronijus","download_url":"https://codeload.github.com/petronijus/Mindbaboon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/petronijus%2FMindbaboon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34907985,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apscheduler","docker","flask","goal-tracker","mcp","python","self-hosted"],"created_at":"2026-06-29T00:02:35.121Z","updated_at":"2026-06-29T00:02:41.794Z","avatar_url":"https://github.com/petronijus.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mindbaboon\n\nGoal tracker with iteration-based email reminders. You set a goal, pick a\ncadence (week / 2 weeks / month), and the app emails you to check in.\nResponding to the email feeds a small history log so you can see the\ncycle of attempts over time.\n\nStack: Flask + SQLite + APScheduler, packaged as Docker. UI gated behind\nGoogle OAuth (email allowlist). Includes a REST API and an MCP server so\nyou can manage goals from an LLM client (e.g. Claude Code) or any HTTP\ntool — the API uses an independent `X-API-Key` so MCP/cron jobs keep\nworking without browser sessions.\n\n## Architecture\n\n| Piece | What it is |\n|---|---|\n| `mindbaboon.py` | Flask app (UI routes: `/`, `/add`, `/edit`, `/settings`) |\n| `iteration.py` | Blueprint handling the iteration response flow (`/iteration/\u003cid\u003e`) |\n| `api.py` | REST API blueprint (`/api/...`), API-key auth |\n| `scheduler.py` | APScheduler + email sender, persistent via SQLAlchemyJobStore |\n| `database.py` | SQLite helpers |\n| `init_mindbaboon_db.py` | Idempotent schema init |\n| `mcp_server/` | Standalone MCP server wrapping the REST API |\n\nTimers survive container restarts: APScheduler stores jobs in the\n`apscheduler_jobs` table in the same SQLite DB as goals, so `next_run_time`\nis recovered on boot. Missed fires within `misfire_grace_time` (60s) run\non startup; older misses are dropped (`coalesce=True` collapses repeats).\nA separate idempotency guard in `send_goal_reminder` refuses to send a\nreminder if one was already sent for the same goal within the last hour\n— defense against restart races causing duplicate emails.\n\n## Run\n\nProduction deployment runs on a Proxmox host (LXC or VM with Docker).\nLocal Mac/dev box is only used to build and ship — never to \"host\" the\nservice. The pieces:\n\n1. Copy `.env.example` to `.env` and fill in SMTP creds and an API key:\n   ```bash\n   cp .env.example .env\n   python -c \"import secrets; print(secrets.token_hex(32))\"  # paste into MINDBABOON_API_KEY\n   ```\n2. On the Proxmox-hosted target, in the project directory. Either pull the\n   prebuilt image from GHCR (recommended) or build from source:\n   ```bash\n   docker compose -p mindbaboon pull \u0026\u0026 docker compose -p mindbaboon up -d   # prebuilt (ghcr.io/petronijus/mindbaboon)\n   # or, to build locally instead:\n   docker compose -p mindbaboon up -d --build\n   ```\n   The `-p mindbaboon` is load-bearing — compose uses it to name the\n   data volume (`mindbaboon_mindbaboon_data`). Changing it strands your\n   existing DB.\n3. Open `http://\u003cproxmox-host\u003e:5000` to confirm the UI loads.\n\n### Critical: never run two instances against the same DB\n\nThe scheduler uses a SQLite-backed job store with no inter-process\nlocking. If you start the app twice against the same database (two\ncontainers, container + bare-metal process, two compose stacks), each\nprocess will independently fire every scheduled job and you'll get\nduplicate emails — both startup mail and reminder mail.\n\nSymptoms and diagnostics:\n\n```bash\n# 1) Confirm there's exactly one container\ndocker ps --filter \"name=mindbaboon\"\n\n# 2) Check the live process count from inside the container\ndocker exec \u003ccontainer\u003e ps aux | grep python\n\n# 3) Grep the logs — should see exactly one \"STARTUP\" line per real boot\ndocker logs \u003ccontainer\u003e 2\u003e\u00261 | grep STARTUP\n\n# 4) Hit /api/health and note the \"pid\" + \"hostname\" fields. Repeated\n#    calls should always return the same pid until you restart. If pid\n#    flips between two values, that's two processes load-balancing.\ncurl http://\u003cproxmox-host\u003e:5000/api/health | jq '.pid, .hostname'\n```\n\nThe app also self-defends: startup emails sent within 5 minutes of an\nearlier one are skipped (logged as `WARNING - Skipping startup email`),\nand reminder emails for the same goal within an hour are skipped\n(`WARNING - Skipping reminder for goal X`). If you see those warnings,\nyou have two instances and should track down the second one.\n\n## Authentication\n\nUI is gated behind **Google OAuth 2.0** (authorization code flow with\nPKCE). On first visit to any UI route, an unauthenticated user is\nredirected to `/login`, signs in with Google, and the backend verifies:\n\n1. ID token signature against Google JWKS (via Authlib)\n2. `email_verified=true` claim\n3. Email is in `ALLOWED_EMAILS` (lowercase exact match)\n\nA 30-day Flask session cookie (`__Host-` prefixed, Secure+HttpOnly+\nSameSite=Lax) is set on success. Logout is POST-only with CSRF token.\n\n**API endpoints (`/api/...`) are NOT gated by OAuth** — they keep the\n`X-API-Key` header check from `api.py`. This lets MCP servers, cron\njobs, and curl scripts keep working independently of browser sessions.\n\n### Setup (Google OAuth)\n\n1. https://console.cloud.google.com/ → APIs \u0026 Services → Credentials\n2. Create OAuth 2.0 Client ID, type **Web application**\n3. Authorized redirect URI: `https://\u003cSERVER_HOST\u003e/oauth2/callback`\n4. Drop `GOOGLE_OAUTH_CLIENT_ID` + `GOOGLE_OAUTH_CLIENT_SECRET` into `.env`\n5. Set `ALLOWED_EMAILS=you@example.com,...` and `FLASK_SECRET_KEY=...`\n\n### Defense in depth (recommended)\n\nFor belt-and-suspenders security, add a **Cloudflare Access** policy\nin the Zero Trust dashboard on the same hostname. Cloudflare gates the\nedge with your email allowlist before any request hits the docker host.\nThe app's own OAuth still works for direct LAN/Tailscale access.\n\nSecurity headers (HSTS, CSP, X-Frame-Options, etc.) are set by\nFlask-Talisman. CSRF tokens via Flask-WTF on every POST form.\nRate limiting on `/oauth2/callback` via Flask-Limiter.\n\n## REST API\n\nBase URL `/api`. Every endpoint except `/api/health` requires the\n`X-API-Key` header.\n\n| Method | Path | |\n|---|---|---|\n| GET | `/api/health` | Scheduler state, job count, each job's next_run_time |\n| GET | `/api/goals?include_completed=false` | List goals |\n| POST | `/api/goals` | Create goal (+ schedule if `iteration` set) |\n| GET | `/api/goals/\u003cid\u003e` | Single goal |\n| PATCH | `/api/goals/\u003cid\u003e` | Update allowed fields |\n| DELETE | `/api/goals/\u003cid\u003e` | Delete goal and its history |\n| POST | `/api/goals/\u003cid\u003e/complete` | Record iteration completion |\n| POST | `/api/goals/\u003cid\u003e/snooze` | Pause reminders |\n| POST | `/api/goals/\u003cid\u003e/resume` | Resume and reschedule |\n| GET | `/api/goals/\u003cid\u003e/history` | Goal + iteration history |\n\nValid `iteration` values: `\"week\"`, `\"2 weeks\"`, `\"month\"`. The\n`timedelta` kwargs behind these keys live in `config.py`. Note that\n`\"month\"` is implemented as `weeks=4` (28 days), not a calendar month —\nover a year this drifts by ~5 days. If you need true calendar-month\ncadence, swap the `IntervalTrigger` for a `CronTrigger`.\n\nQuick check after a restart:\n\n```bash\ncurl http://\u003cproxmox-host\u003e:5000/api/health\n# scheduler_running: true, jobs[] with future next_run_time, pid + hostname\n```\n\n## MCP server\n\n`mcp_server/` exposes the REST API as MCP tools so an LLM can create\ngoals, mark iterations done, snooze, and query state. Setup is in\n[mcp_server/README.md](mcp_server/README.md).\n\n## Env vars\n\n| Var | Purpose |\n|---|---|\n| `EMAIL_SMTP_SERVER`, `EMAIL_SMTP_PORT`, `EMAIL_USERNAME`, `EMAIL_PASSWORD` | SMTP for outbound mail |\n| `DEFAULT_TO_ADDRESS` | Who receives reminders |\n| `SERVER_HOST` | Hostname used in email reminder links |\n| `MINDBABOON_API_KEY` | Required to call `/api/...` (except `/health`) |\n| `FLASK_SECRET_KEY` | 32-byte hex, signs session cookies. Hard-fail if missing |\n| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | Google OAuth Web client |\n| `ALLOWED_EMAILS` | Comma-separated lowercase emails permitted to sign in |\n| `TZ` | Timezone for scheduler (default `Europe/Prague`) |\n\nNever commit `.env`. It's in `.gitignore`. `docker-compose.yml` loads it\nvia `env_file:`.\n\n## Data\n\nSingle SQLite DB at `/app/data/mindbaboon.db` inside the container,\nbacked by the `mindbaboon_mindbaboon_data` named volume when running\nwith `-p mindbaboon`. Tables:\n\n- `goals` — the goals themselves. `is_silenced=1` means the next scheduled\n  tick will skip sending email (set when a reminder fires, cleared when the\n  user responds via `/iteration/\u003cid\u003e` or `/api/.../resume`).\n- `goal_history` — one row per completed iteration, with reflection text\n- `iteration_history` — status transitions (Scheduled / yes / no)\n- `apscheduler_jobs` — serialized jobs + `next_run_time` (binary blob,\n  written by APScheduler)\n- `settings` — key/value for things like `default_email` and the global\n  iteration slot (`iteration_weekday` / `iteration_hour` / `iteration_minute`)\n\nFK `ON DELETE CASCADE` on `goal_history` and `iteration_history` is wired\nup — `get_db_connection()` enables `PRAGMA foreign_keys = ON`, so deleting\na goal automatically removes its history rows.\n\n## Releases\n\nThe image is **built and pushed to GHCR locally** (not in CI):\n\n```bash\necho \"$GHCR_PAT\" | docker login ghcr.io -u petronijus --password-stdin   # PAT: write:packages\nscripts/build-push.sh        # builds + pushes ghcr.io/petronijus/mindbaboon:{VERSION,latest}\n```\n\nTagging `vX.Y.Z` (via the `repo-release` flow) then only validates the Docker\nbuild in CI and cuts a GitHub release with notes — it does **not** publish the\nimage. The version is the `VERSION` file (also in the footer and `/api/health`).\nDeploy pulls the prebuilt image (`docker compose -p mindbaboon pull \u0026\u0026 up -d`).\nSee [CHANGELOG.md](CHANGELOG.md).\n\n## License\n\n[MIT](LICENSE)\n\n## Secrets\n\n`.env` is gitignored and kept SOPS-encrypted in the private overlay\n(`private/config/env.sops`) so it syncs across machines without plaintext in\ngit. With the overlay cloned into `./private` and the 1Password CLI signed in,\nrun `./scripts/secrets-decrypt.sh` to materialize `.env` (the age private key is\nfetched from 1Password), or `./scripts/secrets-edit.sh` to change it.\nPrereqs: `brew install sops age`.\n\n\u003e **Do not edit `.env` by hand to make a change stick.** It is a generated\n\u003e artifact — the next `secrets-decrypt.sh` overwrites it from `env.sops`. To\n\u003e change any value (e.g. `SERVER_HOST`), run `secrets-edit.sh`, commit the\n\u003e updated `private/config/env.sops`, then on the host re-run\n\u003e `secrets-decrypt.sh` and `docker compose -p mindbaboon up -d` to apply it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpetronijus%2Fmindbaboon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpetronijus%2Fmindbaboon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpetronijus%2Fmindbaboon/lists"}