Something went wrong. Try again.
a web app for chore tracking
Something went wrong. Try again.
OCaml 93%
4%
Dune 2%
Nix <1%
README.md
Recurring Tasks #
A small "things to do regularly" tracker: each task has a name, an interval in days, and an optional category. The list is sorted by urgency (overdue first), with categories grouped as sections.
Backend: Dream (OCaml) + SQLite. Frontend: server-rendered HTML + Datastar for reactivity — no JS build step, no client framework.
Project layout #
dune-project
bin/
dune
main.ml -- entry point
lib/
dune
db.ml -- SQLite schema + CRUD
status.ml -- "due in N days" / "overdue" calculations
render.ml -- HTML templates (full page + fragments)
sse.ml -- Datastar SSE helpers (patch-elements/patch-signals)
handlers.ml -- HTTP route handlers
app.ml -- router + Dream.run
Setup #
You'll need OCaml + opam. Then:
opam install dream sqlite3 yojson lwt_ppx
dune build
dune exec bin/main.exe
The app listens on http://localhost:8080 and creates
recurring_todo.db (SQLite) in the working directory on first run.
How it works #
GET /renders the full page: the "add task" / "add category" form plus a<div id="task-list">containing the current tasks, grouped by category and sorted by due date.- Each "Done", "Delete", "Add task" and "Add category" button is wired
up with
data-on:click="@post(...)"/@delete(...). Datastar sends the current signal store (the form's bound inputs) as JSON in the request body. - Every handler responds with
text/event-streamand sends adatastar-patch-elementsevent containing a freshly-rendered<div id="task-list">...</div>. Since the response element has the sameidas the one already on the page, Datastar morphs it in place — re-sorted, re-grouped, with the new/updated/removed task reflected. - Adding a task or category also sends a
datastar-patch-signalsevent to clear the relevant input fields, and adding a category re-sends the<select id="category-select">so the new option appears immediately.
Data model #
categories(id, name)tasks(id, name, category_id, interval_days, last_done, created_at)
last_done is NULL until a task is completed for the first time, in
which case the "due" clock starts from created_at. Marking a task done
sets last_done to the current time; "due again" = last_done + interval_days.
Urgency buckets (see lib/status.ml):
- Overdue — past its due date (red)
- Due soon — due today, tomorrow, or within 3 days (amber)
- Upcoming — everything else (green)
Ideas for extending this #
- Edit tasks: add a
PUT /tasks/:idroute and an inline edit form (toggle visibility with a signal, e.g.data-show="$editing123"). - Snooze: a
/tasks/:id/snoozeendpoint that pusheslast_doneforward by a day or two without marking the task fully "done". - History: a
task_completionstable logging every "done" event, to show streaks or a completion log per task. - Auth/multi-user: add a
userstable and auser_idcolumn ontasks, plus Dream's session middleware. - Live updates across devices: keep the SSE connection open (instead
of one-shot per action) and push
datastar-patch-elementswhenever any client changes data — useful if multiple people share a household task list.