Files
minabot/docs/API.md
syntaxbullet 00729096bf feat: add theme provider and control components for light/dark mode
- Implemented ThemeProvider to manage theme state and preferences.
- Added ThemeControl component for users to switch between system, light, and dark themes.
- Integrated localStorage for theme preference persistence.
- Updated document styles based on theme changes.

test: add tests for permanent deletion of habits and related records

- Created tests to ensure permanent deletion of habits removes all dependent records.
- Verified that deletion requires ownership, authentication, and checks for archived habits.
- Added tests to confirm rollback behavior on deletion failures.

test: add tests for habit palette and progress card functionality

- Implemented tests for habit color progression and theme-based color shading.
- Added tests for progress card calculations and calendar display logic.

feat: define theme types and preferences

- Introduced Theme and ThemePreference types for better type safety.
- Created utility functions for resolving theme based on user preference and system settings.
- Defined theme palettes for light and dark modes to ensure adequate contrast.
2026-09-05 10:07:28 +02:00

17 KiB
Raw Permalink 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
DELETE /habits/:id/permanent Permanently delete an owned archived habit and all its history; 204. Active habits return 409. Remove it from combined charts and delete charts left empty.
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",
  "color": "#426582",
  "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.

Creation and habit PATCH accept an optional six-digit hex color for all three methods. It is saved atomically as the habit calendar's mainColor, separately from dated requirements. Omitting it preserves the default on creation or the current color on edit. Habit PATCH preserves all other calendar settings; the calendar-settings endpoint remains available for replacing the complete settings object.

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. Habit configuration responses include warnings: no_tasks for an empty task habit, or task_never_due with a taskId when the parent and task recurrence rules can never coincide. These are informational and do not prevent saving. The check uses the exact repeating schedules, including anchored day/week intervals. 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, closedAt and updatedAt. Occurrences follow the current dated task-definition order.

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. Temporarily archiving the habit or disabling its schedule preserves same-day task states for restoration. 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. The closedAt timestamp finalizes the original outcome at the deadline exactly once, including tasks that were completed. A historical correction changes done without clearing or inventing 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; very light main colors use darker intermediate shades so white and near-white choices remain distinct. 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. PATCH preserves all omitted top-level fields and rejects an empty patch. Request bodies are fully read before taking the dated configuration snapshot, so overlapping mutations in the single Bun server do not reuse stale configuration. A request streaming across midnight uses the date at which its write is processed. Explicit progress writes remain absolute totals; simultaneous edits of the same field use the last processed value. Failed domain transactions roll back their partial writes. Progress audit events record before/after values; they are not overwritten by corrections.

API readiness review

The API regression suite and production smoke checks cover the known defects found before UI work: omitted PATCH fields, same-day task restoration, immutable expiry, correction audit values, overlapping configuration writes, streaming requests across midnight/timezone changes, stable task ordering, and pale calendar colors. Migration 0003 reconstructs deadline outcomes for existing active occurrences using the first post-deadline correction's before-state when present.

PRD acceptance criteria Evidence
1: exclusive completion method security.test.ts, calendar.test.ts
23: count target and binary manual progress api.test.ts, calendar.test.ts
45: derived task progress and no-due exclusion api.test.ts
69: equal-weight combined ratios, neutral and full states api.test.ts, shared/calendar.test.ts
10: fixed daily, day-interval, weekday and week-interval recurrence calendar.test.ts, api.test.ts
11: occurrence expiry and fresh next occurrence api.test.ts, regressions.test.ts, migrations.test.ts
12: historical corrections without shifting recurrence api.test.ts, carryover.test.ts, regressions.test.ts
13: preserved historical requirements api.test.ts, resilience.test.ts, regressions.test.ts
14: exact values and status for inspection api.test.ts, shared/calendar.test.ts

Habit test filenames above live in src/habits, migration tests in src/db. The test:smoke command independently checks built production HTTP behavior and persistence after restart. Line coverage is a supplementary metric; these behavior checks are the acceptance evidence. Actual calendar rendering, interaction, responsive layout, and accessibility remain UI-layer work. Live Discord consent continues to require the configured provider and a real user login; tests stub its network calls. Optional account export, reminders, streaks and other features outside the PRD are not prerequisites for the UI layer.