Demo Read-only portfolio demo — nothing you change here is saved.

Login

About the project

Productivity app

A self-hosted Flask app for planning the week: projects with a Markdown plan, a drag-and-drop
timeline, three project slots a day and a stopwatch for tracked time. One SQLite file, one
container, no external services.

The project view: a Markdown plan rendered as colour-coded, checkable sections

What it does

  • Projects — each one has a short goal and a long plan written in Markdown.
    Headings become steps, checkboxes stay checkable in the rendered view, and a finished
    section can be archived out of the plan without leaving the page. Beside the plan sits
    Statistics, which holds nothing until it is asked: press Show and it works out, from the
    last three weeks, how many sessions a week the project came round and how long a session ran.
    Nothing is stored — the figures are counted from the bookings and the clock when you ask for
    them, so they cannot go stale. Archiving the whole project
    takes it out of the planning — off the home page, out of the block picker on the schedule, out of
    ⌘K — while the sessions already booked for it stay in the schedule and still count; the project
    page says so, and one still due today keeps its place in ⌘K. A project marked private
    carries no padlock anywhere — that would point it out. Switch on safe mode (the shield next
    to the theme switch) and such a project opens with its plan and its thoughts behind a button
    each; a reveal is remembered for five minutes, and reaching for the shield again drops it.
  • Tags — write #anything in a list item of a plan and it becomes a tag. Nothing is stored
    as a tag: the tag page, linked from the home page, reads the plans as it opens — spinner and
    all — and groups what it finds; every entry leads back to the line it came from. Tags are
    picked out in the plan itself too.
  • Timeline — projects and free-form notes arranged in columns you drag between; a backlog
    column holds whatever is off the timeline.
  • Day slots — every day has slots A, B and an optional C, one project each. The home page
    shows what is in them, the first heading of each plan and today's tracked time.
    Below that sits a health ring, 0 to 100: how many of the booked sessions in the week before today
    were ticked off. Only finished work moves it. Under it, Show draws how the score moved over
    the last two weeks, one point a day, worked out only when asked for.
  • Schedule — a month of day sheets, each showing its A/B/C blocks in the same colours as
    the home page: dashed grey while free, amber once booked, green when the session is done. The
    sheets are a live board with no edit mode to switch on: click a free block to fill it, drag a
    project between blocks — dropping one on a taken block swaps the two — or free a block with its ×.
    A session that keeps getting put off says so: moving one onto a later day reddens its block —
    once for the first move, the rest of the way for the second — and marks it with a ! that
    says, on hover, how many times it has happened. Pulling it back to an earlier day takes a move
    off again, one for one: a session put off four times takes four moves back before it is clear,
    even though it stopped getting redder after two. The count belongs to the booking, so freeing
    the block and booking the project again starts it clean, and a finished session is green
    whatever it took to get there.
    Under the blocks, behind the same dashed line the date sits behind, is that day's note list:
    a + adds a line, a click on one rewrites it where it stands, a × takes it away, and a day takes
    as many as get written. Above them sit that day's events from any calendar you have
    subscribed to — see Calendars below.
    "Day off", next to the archive link, asks for a date and frees it: that day and everything
    planned on it and after it — notes included — move one day later, bar a session already ticked
    off, which stays on the day it happened; the page shows as many weeks as it takes to keep the
    last booking in view.
    Archive shows the same sheets for days already gone, three weeks a page, back to the first
    booking; it is a record, so nothing there can be booked, moved or freed — but its ✓ still works,
    so a session finished on Tuesday can be ticked off on Thursday and counts towards the health
    ring like any other. The note list is live there too: a day is usually only described once it
    has been.
  • Tasks — things to do, each on a day or on none. The Tasks tab lists all of them by
    date: whatever is overdue, today, tomorrow, every later day with something on it, the ones
    with no date, and the finished ones folded away under Completed. The same tasks show up on
    the schedule, on the sheet of their own day between the blocks and the notes, and in a box
    on the home page under today's calendar — today's tasks, with anything left undone on a day
    already gone above them. They are edited the same way everywhere, in place: the circle ticks
    one off, clicking the text rewrites it, "+ Task" adds one to that day, and the ⋯ (or a right
    click, or a long press on a phone) moves it to today, tomorrow, the day after, the weekend,
    next week or any day you pick, takes its date away, sets it repeating, or deletes it. "Day
    off" moves the open tasks along with the bookings; a finished one stays on the day it was done.
    A task can repeat every day, every weekday, every week, every two weeks, every month or
    every year. Only the next occurrence exists: ticking it off keeps it as done and writes the
    one after it on its date, so the schedule is never filled with copies; taking the tick back
    takes that new one away again. A ↻ beside the text marks a repeating task.
  • Inbox — somewhere to put a thought before deciding which project it belongs to. Long-press
    the installed app's icon on Android and the menu offers Add to inbox, which opens straight
    onto a box: type it, or dictate it with the mic on the keyboard, and it is saved without today's
    plan ever coming up to argue with it. There is a + beside "Today" for the same thing at a desk.
    What is waiting shows up in a list on the home page, each line with a project picker; choosing
    one appends the text to that project's thoughts, a blank line below whatever was already there,
    and takes the line off the list. A × throws one away instead — something captured by accident
    should not have to be filed somewhere real to be got rid of. An empty inbox shows nothing at
    all.
  • Calendars — paste a calendar's address in iCal format on the Integrations page (under
    your name in the top right) and its events show up under the blocks of every day sheet, in the
    past as well as ahead. The public and the private ("secret") address both work; nothing is ever
    written back, so the private one just shows more. It is the whole integration: no OAuth, no
    client, no token to expire — a URL that is re-read at an interval you set there (half an hour
    by default), never while a page is loading: the schedule renders from the last copy and fetches
    the new one once it is up, with a spinner in the corner while it does. The copy is cached, so a
    calendar that goes down leaves the sheets as they were, and one button takes a calendar off them
    without forgetting its address. Recurring events, all-day events, exceptions and moved
    occurrences are understood; the events themselves are read-only here, because the calendar owns
    them.
  • Time tracking — start/stop a timer per project, with daily and weekly totals. Deleting a
    project keeps its entries: they hold a snapshot of the title, so past weeks stay correct.
  • JSON API — /api/v1 exposes today's slots and start/stop for the timer, authenticated by a
    bearer token you generate on the account page. It is what the macOS menu bar client talks to;
    the token is separate from your password, so changing one leaves the other working.
  • Installable (PWA) and mobile-first, because most of the ticking off happens on a phone.

Stack

Flask 3 · SQLAlchemy + Alembic · Flask-Login · SQLite · Jinja templates with vanilla JS ·
Gunicorn in Docker behind nginx.

Run it locally

python -m venv .venv && source .venv/bin/activate   # .venv\Scripts\activate on Windows
pip install -r requirements.txt
cp app/instance/.env.example app/instance/.env      # every setting is documented in there
python run.py

The app starts at http://127.0.0.1:5001 and creates the SQLite database and its tables on
first run. Register an account, or set DEFAULT_LOGIN_EMAIL / DEFAULT_LOGIN_PASSWORD to
pre-fill the login form.

Port 5001 rather than Flask's usual 5000: on macOS the AirPlay Receiver in Control Center
listens on *:5000 and answers every request with a 403.

Deployment

An instance is a directory with a docker-compose.yml and an app/instance/.env; the same
compose file serves the private instance and the public demo, which differ only in that file.
Nothing is published on the host — the container has a pinned name and a reverse proxy reaches
it by that name on port 8000, so several instances can share one server. On first boot the
entrypoint generates a SECRET_KEY, runs the migrations and, in demo mode, seeds sample
content; all three are idempotent, so redeploys leave the data alone. There is more info about demo in demo/example.

Docs