Morgen.so is expensive and overloaded. Calendula does one thing well: focused Eisenhower task planning for Microsoft 365 — self-hostable, installable as a PWA, no AI bloat.
| Calendula | Morgen.so | Other tools | |
|---|---|---|---|
| Price | Free (OSS) | Subscription | Mostly free |
| Eisenhower + Time-Blocking | ✅ | ✅ + AI + everything | Usually no |
| Microsoft 365 native | ✅ | Multi-provider | Varies |
| No AI bloat | ✅ | Heavy AI | Trend-following |
| Self-hostable / PWA | ✅ Docker + PWA | Cloud-only | Varies |
⚠️ Single-user app: Calendula is designed for one operator. App-level authentication (scrypt + HMAC session) is enabled by default since 0.4.0 (#205). On first start the frontend redirects to/setupto set a password. See Authentication below and docs/security.md.
The screenshots below use mock task data — the UI is the real app.
- Backend: FastAPI + Microsoft Graph API (O365 To Do + Calendar)
- Frontend: SvelteKit + FullCalendar.js + Tailwind CSS
- Auth: MSAL Python (Delegated — Device Code Flow, Token Cache)
- Event-Log: SQLite + SQLAlchemy (status changes, undo — NOT for task data)
- Container: Docker Compose
Step 0 — Set up Azure App Registration (one-time, ~5 minutes): See docs/azure-setup.md.
Then:
cp .env.example .env # fill in AZURE_TENANT_ID + AZURE_CLIENT_ID from Step 0
docker compose up- Frontend: http://localhost:5173
- Backend: http://localhost:8000
- Health check:
curl localhost:8000/health - First start: Device Code Flow — URL and code appear in backend logs
The UI is currently German-only. English translation (i18n) is on the post-OSS-launch roadmap — see #233. Docs (README, setup guide) are in English. Contributions welcome.
Frontend (SvelteKit) --- API ---> FastAPI
|
TaskProvider ABC
|
O365TaskProvider
|
GraphClient (MSAL)
|
Microsoft Graph API
- To Do Tasks (CRUD + Extensions)
- Calendar Events (Time-Blocking)
- Open Extensions (schedule data)
- Categories (taskId linking)
All task data lives in O365. Calendula has no own task database.
- Task lists from all O365 To Do lists (including Flagged Emails with Outlook deep-links)
- Eisenhower prioritization (P1-P4) via O365 categories
- Time-blocking via drag-and-drop into calendar (creates O365 Calendar Events)
- Completed tasks visually dimmed in calendar (opacity + strikethrough)
- Standard calendar overlay (read-only, dimmed) alongside task time-blocks
- Subtasks (O365 checklist items)
- Recurrence display
- Due dates + reminders
- Matrix view — Eisenhower matrix (P0–P4) with drag-drop between quadrants
- Focus view — Decision-helper mode: pick one task to work on, timer-based single-task UI
- Done view — Completed tasks archive
- Mutable settings — Calendar selection, timezone, work hours, slot duration are runtime-editable (no restart)
- Hot-reload — Settings changes apply on next request (no container restart)
- Undo — Local event log tracks status changes; recent changes can be undone
- Installable as PWA — Home-screen shortcut on desktop and mobile, offline app shell via service worker
Flagged emails from Outlook appear in the "Flagged Emails" list. The detail popup shows an "In Outlook oeffnen" link.
Tip: To open mail links in the Outlook desktop app instead of the browser, enable "Open links in desktop app" in your O365/Outlook Web settings.
Calendula ships with built-in password authentication (APP_AUTH_ENABLED=true by default
since 0.4.0, #205).
For OSS self-hosters: a fresh deploy without explicit auth config used to expose the API on whatever port the reverse proxy exposed. A log warning is not enough — the safe default is on, with a graceful first-start flow instead of a hard error.
- Start the stack — backend logs print two warnings:
APP_AUTH_ENABLED=true ... aber noch kein Passwort gesetzt. Erstes Passwort via /setup ...Setup token generated: see data/calendula_setup_token (chmod 600) ... - Grab the setup token (#215)
— it gates POST
/api/app-auth/setupso a random external POST cannot hijack a fresh deployment in the seconds between startup and your first browser tab:Alternative: setdocker exec <container_name> cat /app/data/calendula_setup_token
CALENDULA_SETUP_TOKEN=<your-token>as env-var (Komodo stack UI /.env). The env-var takes precedence and the file is never generated; useful if you provision the token via secret manager. - Open the frontend in your browser. The layout detects
setup_complete: falseviaGET /api/app-auth/statusand redirects to/setup. - Paste the setup token into the form, set a password (min. 8 characters). Hash is stored
as
auth.password_hashin the config store (SQLite-backed JSON, persisted incalendula_datavolume). The token file is deleted after success. - Login screen appears — log in with the password you just set.
- Session cookie
calendula_session(HttpOnly, SameSite=Strict, 7 days) is set; the normal app loads.
For non-browser clients (curl, scripts): protected routes return
401 {"detail": "setup_required", "setup_url": "/setup", "message": ...} until a password
is set via POST /api/app-auth/setup {"password": "...", "setup_token": "..."}. A missing
or wrong token returns 403 {"detail": "setup_token_invalid"}.
Migration note for existing self-hosters: if a password is already configured (config
store has auth.password_hash), the token check is skipped — the endpoint returns the
familiar 403 setup_already_done as before. No action required on upgrade.
Set APP_AUTH_ENABLED=false in .env / stack env. The ASGI middleware is not registered
and the API is fully open. Use only if an upstream layer (Caddy IP-whitelist + VPN, network
isolation, mutual TLS) is enforcing access control. A clear warning is logged on every start.
PUT /api/app-auth/password with the current session cookie, body
{"old_password": "...", "new_password": "..."}. Existing session tokens remain valid until
they expire (no server-side revocation list).
For production behind HTTPS: forces the Secure flag on the session cookie. Default off so
local-development on http://localhost:5173 works without certificate gymnastics.
External contributions are welcome. See CONTRIBUTING.md for setup, branch-naming, commit conventions, and PR workflow.
When serving Calendula under your own domain, set CORS_ORIGINS in .env to the list of
origins that may call the API. Accepts a comma-separated list (or JSON array). The default
covers only localhost dev ports — production browsers will be blocked by CORS until your
FQDN is added. Example: CORS_ORIGINS=https://calendula.example.com,http://localhost:5173.
# Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
# Frontend
cd frontend
npm install
npm run devVerify your changes against the App-Auth + CORS path without touching the live deploy:
cp .env.smoke.example .env.smoke # fill AZURE_TENANT_ID + AZURE_CLIENT_ID
docker compose -f docker-compose.smoke.yml --env-file .env.smoke up -d --build
python3 scripts/smoke_test.py # 9 HTTP assertions, exits non-zero on failure
docker compose -f docker-compose.smoke.yml --env-file .env.smoke down -vSmoke uses isolated ports (8001/5174) and volumes (calendula_smoke_data) so it runs in parallel to a live container.
See docs/azure-setup.md for the step-by-step setup guide with screenshots.
Summary:
- Permissions (Delegated): Tasks.ReadWrite, Calendars.ReadWrite, User.Read, offline_access
- Public Client Flows: Enabled (for Device Code Flow)
- Credentials in
.env(not in repo)
Calendula uses Semantic Versioning. All notable changes are tracked in CHANGELOG.md.
curl https://<host>/api/version
# {"version":"0.4.0","commit":"a20d601","built_at":"2026-06-01T10:30:00Z"}git pull
docker compose -f docker-compose.prod.yml up -d --buildVia a stack manager (Portainer / Komodo / Dockge / etc.): trigger a redeploy of the stack. Set BUILD_COMMIT and BUILD_TIMESTAMP build args to inject version metadata into the /api/version response.
Calendula is preparing for an OSS release. Tracked in Forgejo:
- #156 Native auth — Local password + optional session cookies (scrypt + HMAC). Epic in progress, 4 sub-packages.
- #109 Versioning — SemVer,
/api/versionendpoint, Docker image tags. - #157 Multi-calendar overlay — N read-only overlay calendars with per-calendar color + blur.
- #9 PWA — Installable app for desktop + mobile (manifest + service worker).
Once #156 + #109 ship, the built-in-authentication warning at the top will be replaced by standard OSS setup docs.
Calendula runs on any Docker host. The compose files (docker-compose.yml for dev,
docker-compose.prod.yml for production) work standalone or with stack managers like
Portainer, Komodo, or Dockge.
- Compose file:
docker-compose.prod.yml - Backend: built from
backend/Dockerfile.prod(no bind mounts, onlycalendula_datavolume) - Frontend: built from
frontend/Dockerfile.prod(multi-stage; SvelteKitnode build/index.json port 3000) - Persistence: named volume
calendula_dataholdsmsal_token_cache.json+calendula.db
AZURE_TENANT_IDAZURE_CLIENT_IDCALENDAR_USER_EMAILTODO_LIST_NAMES(optional, empty = all lists)PUBLIC_API_BASE_URL(defaulthttps://calendula.example.com)PUBLIC_GRAFANA_URLDEBUG_LEVEL(defaultoff)VITE_DEBUG_LEVEL(defaultoff)
- Deploy stack
- Tail backend logs: device code + verification URL appear once
- User opens https://login.microsoftonline.com/common/oauth2/deviceauth, enters code
- Token cache lands in the
calendula_datavolume — subsequent restarts are headless
Any HTTPS reverse proxy (Caddy, Traefik, nginx) works. Route /api/* and /health
to backend (port 8000), everything else to the SvelteKit frontend (port 3000).
Restrict by IP/network as needed.




