Build Height
A personal project tracker whose hard problems are recurrence that behaves the way the calendar standard specifies, saved views that survive a schema change, and due dates that arrive on the day the person meant.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the test suite.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why Hand rolling recurrence is where the skip rules get invented rather than followed, and the standard states them plainly.
Get it A maintained recurrence library for your runtime, or the recurrence rule section of RFC 5545. open ↗
Verify
The monthly rule on the 31st example from the standard produces the dates the standard shows - decidefree
Why The categories are what the code branches on, so inventing them while writing the first list view guarantees a fourth category appears later.
Get it Write down each status, its category of open, done or cancelled, and its order.
Verify
The list is in the seed data and every status maps to one of three categories - have readyfree
Why Recurring tasks, odd due dates and half finished work only exist in a real list, and a seeded demo list will pass every test that matters.
Get it Export your current task list to CSV, or copy fifty real tasks by hand.
Verify
It imports with the dates and the recurrence rules intact - decidefree
Why Date handling looks perfect from a machine set to UTC, which is why the bug reaches everyone else.
Get it Pick a zone at least eight hours from UTC with a daylight saving change and run the suite under it.
Verify
The suite passes with that zone set and the two transition dates included
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- projects(id, name, key, archived_at) - statuses(id, project_id, name, category, ordinal) · category is open, done or cancelled, and code branches on category and never on the name - tasks(id, project_id, title, body, status_id, priority, due_date, due_at, due_zone, created_at, completed_at, archived_at) - recurrences(task_id, rule_text, dtstart_date, dtstart_zone, count_remaining, until_date, materialised_to) · a documented subset of the recurrence rule - instances(id, recurrence_task_id, occurrence_date, task_id, skipped_reason) · materialised ahead, and skipped occurrences are recorded with why - views(id, name, filter_json, sort_json, group_by, ordinal) · a structured document compiled to a query, never stored SQL - activity(id, task_id, at, kind, detail) · the only record of what a task looked like before the last edit
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/tracker.db | Projects, tasks, views and recurrence rules. |
BIND_ADDRESS | required | 127.0.0.1 | The whole access control model. Changing it should require thinking. |
PORT | required | 4321 | Local port for the interface. |
USER_TIMEZONE | required | Pacific/Auckland | Used for today, overdue and this week. Set it to something other than UTC while testing. |
MATERIALISE_AHEAD_DAYS | required | 90 | How far recurring occurrences are generated in advance. |
ARCHIVE_AFTER_DAYS | required | 30 | Completed tasks leave views after this, and stay in exports. |
EXPORT_DIR | required | ./exports | Where the JSON and CSV exports are written. |
The build, in order
Tasks, and a workflow that is data
Statuses are rows with a category, so changing the workflow is not a migration.
Three categories is enough, and it is what the code is allowed to see.
Files
src/db/schema.sqlsrc/statuses.tsA grep in the test suite is a fine way to enforce it.
Files
tests/no-status-name-checks.test.tsOrphaned tasks disappear from lists and reappear as a mystery.
Files
src/statuses.ts
done when · tick each as it passeswatch out- Writing statuses as a fixed list in code means the first workflow change is a migration, and the second one strands tasks in a status the code no longer knows about so they vanish from every list. Store statuses as rows with a category, branch on the category, and make deletion ask where the tasks move.
Views that are saved queries
A named filter keeps working after the schema changes.
Fields, operators and values, each validated against the schema.
Files
src/views/filter.tsEvery value bound, every time, with a test that proves it.
Files
src/views/compile.tstests/filter-injection.test.tsAn empty list looks like an answer and is not one.
Files
src/views/render.tsx
done when · tick each as it passeswatch out- Saving the generated SQL for a filter means renaming a column silently turns a saved view into an empty list, and if any part of that string ever came from user input it is also the injection surface you thought you did not have. Store the filter as data and compile it fresh every time.
Recurrence the way the standard defines it
A repeating task generates the right dates, including the ones that must not exist.
Naming the parts you support is what keeps the behaviour honest.
Files
src/recurrence/parse.tsdocs/recurrence.mdNot clamped, not shifted, and not counted toward the count.
Files
src/recurrence/expand.tstests/recurrence-invalid.test.tsTen dates on screen catches every misunderstanding in one second.
Files
src/recurrence/preview.tsx
done when · tick each as it passeswatch out- Implementing 'every month on the 31st' by clamping to the last day puts a task on 28 February that should not exist, and the standard is explicit: RFC 5545 says recurrence instances with an invalid date or a nonexistent local time must be ignored and must not be counted as part of the recurrence set, so a skipped month must not consume the count either. Skip, record why, and show the generated dates before the rule is saved.
Dates that arrive on the day you meant
Quick entry produces the task and the date the person had in mind.
Different type, different column, and never converted.
Files
src/dates.tstests/date-only.test.tsQuick entry that guesses silently creates the wrong task quickly.
Files
src/quickadd.tsxMidnight is a local event, and every list depends on it.
Files
src/dates.ts
done when · tick each as it passeswatch out- Storing a date only due date as midnight in UTC and rendering it locally makes every task due a day early or a day late depending on which side of the meridian the person is on, and the error is invisible to whoever wrote it and constant for everybody else. Keep date only fields as plain dates and never let them touch an instant conversion.
Everything out, and everything back inproduct builder
An export round trips, including the rules that generate future work.
The rule is the part that makes future work happen.
Files
src/export.tstests/roundtrip.test.tsSame views, same statuses, same next ten dates.
Files
src/import.tsWith no accounts, the interface it listens on is the whole boundary.
Files
src/server.ts
done when · tick each as it passeswatch out- Exporting the materialised task instances and not the recurrence rules produces an import that looks complete and never repeats again, and the loss shows up weeks later as work that quietly stopped appearing. Export the rules, and assert in the suite that the next ten occurrences match before and after a round trip.
That is the whole plan for Height. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multiple users and permissions.
- Real time collaboration.
- An integration catalogue.
- Mobile capture and push notifications.
- enterprise permissions
- large integration catalog
- mobile capture and notifications
- real-time multiplayer
- A board view over the same structured filters, which is a rendering change rather than a new query path
- Dependencies between tasks, once statuses and categories have settled
- A daily digest written to a file, which needs no notification infrastructure at all
Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks.