Files
minabot/docs/API.md

14 KiB
Raw Blame History

Habit REST API

All paths below start with /api. The existing Discord session cookie authenticates every habit, task, chart and settings route. Mutations additionally require Origin to exactly match APP_ORIGIN. JSON bodies require Content-Type: application/json and are limited to 64 KiB. Responses use Cache-Control: no-store. User IDs are taken from the session, never from input. Foreign and nonexistent resources both return 404.

Run bun dev to migrate the local database and serve the API. Run bun run test:coverage for the full test suite, and bun run test:smoke to build and exercise the production server over HTTP using a disposable database, including a restart.

Routes

Method Path Result
GET /health Public database health
GET /auth/discord?timezone=Europe%2FBelgrade Start Discord sign-in
GET /auth/discord/callback Finish Discord sign-in
POST /auth/logout Revoke current session; 204
GET /me Current public user
PATCH /me Update saved timezone: {"timezone":"Europe/Belgrade"}
GET /habits {habits: [...]}; excludes archived by default
GET /habits?archived=true Include archived habits
POST /habits Create habit; 201, resource and Location header
GET /habits/:id Current habit configuration and revision ID
PATCH /habits/:id Change name, method, method settings, schedule or archived flag
DELETE /habits/:id Archive from today, preserving history; 204
GET /habits/:id/history {revisions: [...]} newest first, including superseded same-day revisions
GET /habits/:id/tasks {tasks: [...]} for current task method
POST /habits/:id/tasks Add task; 201
GET /habits/:id/tasks/:taskId Current task definition
PATCH /habits/:id/tasks/:taskId Change task name or recurrence
DELETE /habits/:id/tasks/:taskId Remove task from today's requirements onward; 204
GET /today Tracking date, timezone, all active habits with progress, due and completed counts
GET /days/:date All owned habits for a date, including historical archived habits
GET /habits/:id/days/:date Requirement snapshot, underlying progress, status and task occurrences
PUT /habits/:id/days/:date/progress Replace count or manual state for that date
PUT /habits/:id/days/:date/tasks/:taskId Replace that task occurrence's boolean completion state
GET /habits/:id/days/:date/audit {records: [...]} with all same-day snapshots, occurrences and before/after progress events
GET /habits/:id/calendar?from=YYYY-MM-DD&to=YYYY-MM-DD Individual calendar with every date and its details
GET /habits/:id/calendar-settings Effective color/shade settings
PUT /habits/:id/calendar-settings Replace settings; omitted fields reset to defaults
GET /charts {charts: [...]} owned combined charts
POST /charts Create combined chart; 201, resource and Location header
GET /charts/:id Chart name, selected habit IDs and settings
PATCH /charts/:id Change name, selected habits or settings
DELETE /charts/:id Delete the saved chart view; 204; underlying habits remain
GET /charts/:id/calendar?from=YYYY-MM-DD&to=YYYY-MM-DD Combined calendar
GET /charts/:id/days/:date One combined square and its constituent habit details

Dates must be real YYYY-MM-DD dates, 19709998. Calendar ranges are inclusive, ordered, and at most 1,830 days. Future dates are inspectable but cannot be logged. Dates before a habit was created are not due and cannot be logged. Habit creation begins on the current tracking date; there is no implicit historical creation date.

Create and configure habits

Count habit, with daily resets by default:

{
  "name": "Hydration",
  "method": "count",
  "target": 8,
  "unit": "glasses",
  "carryPartialProgress": false,
  "schedule": { "type": "daily" }
}

Manual habit:

{ "name": "Read", "method": "manual" }

Task habit with independent task recurrence:

{
  "name": "Routine",
  "method": "tasks",
  "tasks": [
    { "name": "Stretch" },
    {
      "name": "Clean desk",
      "schedule": { "type": "weekly", "every": 2, "weekday": 3, "anchor": "2026-08-31" }
    }
  ]
}

A habit has exactly one method. Incompatible settings are rejected. A switch to count needs a target. Switching to tasks begins with an empty task list; use the task routes to add tasks. Task IDs are generated by the server and remain stable across edits. Restore an archived habit with PATCH {"archived":false}. Task edits require restoring an archived habit first.

Names are trimmed, nonempty and at most 200 characters. Count targets are integers from 1 to 10,000. Logged counts are nonnegative integers up to 1,000,000,000 and can exceed the target; completion intensity caps at full. Units default to steps and are at most 80 characters. A task habit supports at most 100 task definitions.

Schedules

Omitted habit or task schedules default to daily. Both rules must match for a task occurrence to exist.

Rule JSON Meaning
Daily {"type":"daily"} Every date
Day interval {"type":"interval","every":3,"anchor":"2026-09-04"} Sept 4, 7, 10, ...; never before anchor
Weekdays {"type":"weekdays","days":[1,3,5]} Monday, Wednesday, Friday
Week interval {"type":"weekly","every":2,"weekday":3,"anchor":"2026-08-31"} Wednesday in the anchor week, then every second week

Weekdays use Sunday=0 through Saturday=6. Weekday lists must be nonempty and unique. Week intervals use Monday-start weeks, with no occurrence before the anchor date. Day intervals support 13,650 days; week intervals support 1520 weeks. Neither lateness nor corrections shift recurrence.

A task habit with no matching tasks is not due, including when its parent schedule and task rules do not intersect. Inspect projected future days or the calendar to see the resulting schedule.

Daily reset and optional carryover

Each scheduled day starts with zero count, an unchecked manual state, and unchecked task occurrences. Partial progress does not carry by default.

Count habits can opt into carryPartialProgress: true. An unfinished count carries to the next scheduled date. A completed or over-target count resets the next scheduled occurrence to zero. Manual habits have no partial state; recurring task occurrences retain the PRD's fresh-occurrence and expiry behavior.

Count writes are absolute totals, including any inherited amount. For example, if a date inherits 7 and the user adds one, write {"count":8}. Writing {"count":0} explicitly clears that date. loggedCount identifies explicit entries; carriedFrom identifies an inherited count's source date. Historical corrections recalculate inherited downstream values until an explicit entry or reset boundary. They never overwrite explicit entries on later dates. Method switches and archived dates break inheritance. Carryover configuration changes are effective today, preserving earlier behavior.

Logging and inspecting progress

PUT /api/habits/:id/days/2026-09-04/progress
{"count":7}

PUT /api/habits/:id/days/2026-09-04/progress
{"done":true}

PUT /api/habits/:id/days/2026-09-04/tasks/:taskId
{"done":true}

The first two bodies apply to count and manual habits respectively. Task progress is derived, so directly writing habit progress for a task habit returns 409. A task write identifies the recurring task ID plus date, not an occurrence ID from a superseded snapshot.

Every successful write returns the updated individual day. Important response fields:

  • date, habitId, name, method, revisionId, requirements: the dated configuration used to evaluate the day.
  • timezone, endsAt: the preserved local-day boundary; endsAt is Unix milliseconds, or null for dates without a stored snapshot.
  • due, future, status: not_due, future, empty, partial, or complete.
  • value, target, unit, ratio, complete: exact progress and capped completion ratio. Not-due ratios are null.
  • loggedCount, carriedFrom: explicit and inherited count information.
  • tasks: due occurrences with id, taskId, name, done, expiredAt and updatedAt.

Backfills use that date's method, target, schedule and task list. They automatically affect every combined chart that selects the habit. Reducing a completed habit below its requirement removes its completed contribution.

History, expiry and timezones

Revisions and prior day snapshots are retained. Changing a target, method, task list or schedule takes effect on the current tracking date. Same-method edits retain today's explicit count/manual state; surviving task IDs retain their occurrence state across task edits. Changing methods starts a fresh current-day record, retaining the superseded record for audit. Configuration responses include effectiveDate and revisionId so clients can explain this behavior.

Incomplete task occurrences expire at the preserved local midnight. Expiry is reconciled lazily before authenticated tracking requests, including after downtime, and stores the original deadline rather than the time the API was accessed. No scheduler process is required. A historical correction changes done without clearing the original expiredAt. Tasks completed at their original deadline have no expiry record. Old occurrences never become overdue tasks on a later date.

Timezone updates first reconcile existing dates under the old timezone. Existing snapshots and deadlines remain fixed; subsequent newly opened dates use the new timezone. The tracking date never moves backward when traveling west across a date boundary. /today.date is authoritative during such a transition, and the new timezone catches up naturally. DST boundaries use actual local midnight, including 23-hour and 25-hour days. UTC millisecond timestamps and local date strings serve different purposes and should not be interchanged.

Combined charts

{
  "name": "Daily essentials",
  "habitIds": ["owned-habit-uuid-1", "owned-habit-uuid-2"],
  "settings": { "mainColor": "#196127", "shadeCount": 4 }
}

Select 1100 unique owned habit IDs. Membership edits recompute the entire chart view using the selected habits' historical requirements; they do not alter those requirements. Archived habits remain selectable for historical review.

Each day reports due, completed, ratio, level, shadeCount, color, status, future and habits. Completed habits have equal weight. Partial progress counts as zero completed habits. A zero denominator has a null ratio and neutral color. Full completion is the highest shade, whether the underlying counts are 1/1 or 4/4. Combined shade levels reserve the highest level for full completion.

Calendar settings and Nivo

The API intentionally stores only product-level appearance preferences:

Field Default Allowed
mainColor #196127 Six-digit hex color
shadeCount "auto" "auto" or integer 220
emptyColor #ebedf0 Due with no progress
notDueColor #f5f5f5 Nothing scheduled
futureColor #dbeafe Upcoming date

Positive colors are shades of the main color. auto uses the dated count target or number of due tasks for individual charts and four positive shades for combined charts. Thus an eight-count habit has eight positive levels plus empty. Explicit shade counts offer a simpler display without changing underlying progress. Manual habits always remain binary. Layout, borders, spacing, typography, callbacks and animation are not persisted settings. PUT replaces individual settings; the settings object in a chart PATCH is also a replacement, with defaults for omitted settings fields.

Calendar responses include kind, from, to, today, timezone, settings, days, and a numeric data summary. Summary values are positive shade levels, 0 for due/empty, -1 for not due, and -2 for future. Exact counts always live in days.

The installed @nivo/calendar integration contract is in src/shared/calendar.ts. It imports:

import { ResponsiveCalendar } from '@nivo/calendar';

toResponsiveCalendarProps(response) returns typed data, an exact per-date color scale, and readable value formatting for this component. It uses distinct numeric indices internally because Nivo's color scale receives only a value, and the same shade number under different historical targets can require different colors. Consumers must use the adapter rather than Nivo's automatic min/max scaling on the summary data. Use days for date inspection and detailed tooltip content. The adapter renders no component or page; frontend UI remains deferred.

Errors

Errors are JSON objects with an error string:

Status Meaning
400 Malformed JSON or missing/wrong JSON content type
401 Missing, invalid or expired session
403 Missing or mismatched Origin on mutation
404 Missing resource, another user's resource or unknown route
409 Future/unscheduled write, wrong current task method, direct task-habit progress write, or task editing while archived
413 Body exceeds 64 KiB
422 Invalid fields, unsupported configuration, invalid dates/range, or a progress body inconsistent with the dated method
500 Unexpected failure; internal details are not exposed

Schemas reject unknown body properties. Failed domain transactions roll back their partial writes. Progress audit events record before/after values; they are not overwritten by corrections.