This document describes the current system structure. It is the primary reference for any AI assistant or contributor working in this repo.
Tasklog is a three-process application as of v2.10. The backend, frontend, and MCP server run as separate processes. They share no code; communication is HTTP.
Browser (LAN-only)
│
├── GET http://localhost:3000 Next.js frontend (React, Tailwind)
│ │
│ └── fetch http://localhost:5115/api/... .NET Web API (ASP.NET Core, EF Core)
│ │
│ SQLite database
│ (TasklogDatabase.db)
└── GET http://localhost:3000/tasks/[id]
└── Server Component fetches from API on the server, returns rendered HTML
claude.ai (public, web + mobile)
│
└── HTTPS https://mcp-tasklog.manudubey.in/mcp
│
└── Cloudflare Tunnel → cloudflared (Termux)
│
└── localhost:5180 → tasklog-mcp (Node, proot)
│
├── OAuth 2.1 endpoints
│ (/authorize, /token,
│ /register, /.well-known/*)
│ backed by mcp/data/auth.db
│
└── GET localhost:5115/api/...
(Tasklog API, LAN-only)
The MCP server is the only public surface. The /api and frontend remain LAN-only.
Tasklog/
├── backend/
│ └── Tasklog.Api/ .NET Web API project
│ ├── Controllers/ HTTP endpoint handlers
│ ├── Data/ EF Core DbContext
│ ├── Migrations/ EF Core schema migrations
│ ├── Models/ Data model classes
│ ├── Services/ Pure domain helpers (RecurrenceRule - parse/validate/advance RRULE + OccursOn schedule membership, v2.14.0/#73; HabitStreak - schedule-aware day streak from check-in dates, v2.16.0/#73; HabitFrequency - "x times a week" weekly count/streak/week-status, v2.18.0/#75; JournalMarkdown - render a day's journal to an Obsidian-compatible markdown note, v3.0/#79; JournalTemplates - code-defined journal template definitions upserted at startup, v3.0/#79)
│ ├── Properties/ Launch settings (ports)
│ ├── Program.cs App startup and service registration
│ ├── appsettings.json Config (connection string, logging)
│ └── TasklogDatabase.db SQLite data file
│
├── mcp/ Node/TypeScript MCP server (v2.10+)
│ ├── package.json Dependencies, build/test scripts
│ ├── tsconfig.json strict, NodeNext, target ES2022
│ ├── src/
│ │ ├── server.ts Hono HTTP entry, middleware wiring
│ │ ├── config.ts Env var loading + production validation
│ │ ├── api-client.ts Typed client for the Tasklog .NET API
│ │ ├── tools/ 29 MCP tools wrapping every API endpoint
│ │ │ ├── tasks.ts 18 task tools (list[+filters]/get/create/update/
│ │ │ │ delete/set-completion/assign-project/set-labels/
│ │ │ │ add-comment/list-comments/delete-comment +
│ │ │ │ log-habit-checkin/undo-habit-checkin/get-habit-checkins/get-habits +
│ │ │ │ bulk-set-completion/bulk-assign-to-project/bulk-set-deadline/bulk-set-priority)
│ │ │ ├── projects.ts 4 project tools (create/rename accept optional color)
│ │ │ ├── labels.ts 4 label tools
│ │ │ ├── time.ts 7 time tools (start/stop/active/log/edit/delete/summary) (v2.19.0)
│ │ │ ├── registry.ts Aggregates and registers all tools
│ │ │ └── result.ts runTool() helper for error mapping
│ │ └── oauth/ OAuth 2.1 authorization server
│ │ ├── store.ts SQLite-backed: clients/codes/access/refresh
│ │ ├── crypto.ts opaqueToken(), pkceVerify()
│ │ ├── crypto.test.ts node:test unit tests for crypto layer
│ │ ├── well-known.ts RFC 9728 + RFC 8414 discovery endpoints
│ │ ├── register.ts RFC 7591 Dynamic Client Registration
│ │ ├── authorize.ts /authorize HTML page + signed flow cookie
│ │ ├── github.ts GitHub OAuth upstream callback
│ │ ├── token.ts /token (auth_code + refresh_token grants)
│ │ └── middleware.ts Origin / Protocol-Version / Bearer auth
│ └── data/ Runtime state (auth.db); gitignored
│
├── frontend/
│ └── src/
│ ├── app/ Next.js App Router pages and layout
│ │ ├── layout.tsx Root layout (header, fonts, body wrapper)
│ │ ├── page.tsx Home route /
│ │ ├── globals.css Tailwind import + font tokens
│ │ └── tasks/[id]/
│ │ └── page.tsx Task detail route /tasks/:id
│ ├── components/ Reusable UI components
│ │ ├── ProjectLayout.tsx Sidebar + task list wrapper, owns activeView (Client Component)
│ │ ├── ProjectSidebar.tsx Project navigation and management (Client Component)
│ │ ├── TasksClient.tsx Task list + add form, filters by activeView (Client Component)
│ │ ├── AddTaskForm.tsx Add task form with optional project dropdown (Client Component)
│ │ ├── AssignProjectButton.tsx Project reassignment on detail page (Client Component)
│ │ ├── DeleteTaskButton.tsx Delete action on detail page (Client Component)
│ │ ├── CompleteTaskButton.tsx Complete/incomplete toggle on detail page (Client Component)
│ │ ├── EditTaskModal.tsx Full task edit (title/deadline/project/labels), diff-and-fan-out save (Client Component, v2.10.2)
│ │ ├── DeadlinePopover.tsx Quick deadline preset picker on the deadline pill (Client Component, v2.10.2)
│ │ ├── BulkActionsBar.tsx Sticky bulk-actions bar for multi-select mode (Client Component, v2.10.4)
│ │ ├── PriorityDot.tsx Small colored priority dot (P1-P3) next to a task title (v2.10.5)
│ │ ├── RecurrencePicker.tsx Recurrence builder (none/daily/weekly/monthly + nth-weekday/interval/Ends) on the add/edit forms; `isHabit` prop un-gates it with no deadline (v2.14.0+, #75)
│ │ ├── RecurringBadge.tsx Repeat glyph + human label for recurring tasks (v2.14.0)
│ │ ├── QuickAddInput.tsx Quick-add title field: inline token highlight overlay + #/@/! autosuggest; Escape/tap un-recognizes a token (#73). v2.15.0+
│ │ ├── TaskSheet.tsx Chip-driven create+edit sheet (modal/bottom-sheet); chips derived from the title, Escape-to-dismiss; replaces AddTaskForm+EditTaskModal (#73). For habits: Due chip hidden + the Schedule chip is a two-mode picker (specific days OR x-times-a-week stepper), saving recurrence XOR weeklyTarget (#75)
│ │ ├── BoardView/BoardCard.tsx Board renderer: columns from groupTasksForBoard (lib/board.ts) + the rich card (#73)
│ │ ├── TaskDoneControl.tsx One done-control for list+card+board: complete checkbox, or a daily check-in toggle for a habit (habits are never completed) (#73)
│ │ ├── HabitsPanel.tsx Right-side habits panel beside the task list, shares habit state with ProjectLayout (#73)
│ │ ├── HabitsClient.tsx Full /habits view: fetch + poll habits, optimistic done-today toggle (Client Component, v2.16.0)
│ │ └── HabitCard.tsx One habit: specific-days view (schedule label, day streak, 7-day dot row) OR frequency view (n/x this week, week streak, coloured recent-week strip) keyed on weeklyTarget; shared CheckInButton (v2.16.0/#73/#75)
│ │ ├── SubtaskChecklist.tsx Inline tickable subtask circles clubbed under a parent (card + table sub-row + board), cap + "+N more" (#78)
│ │ ├── SubtaskSection.tsx Full subtask editor (detail modal + /tasks/:id): add/tick/deadline/delete + @dnd-kit drag-reorder (#78)
│ │ ├── CompleteWithSubtasksDialog.tsx Complete-all vs pull-out prompt when completing a parent with open subtasks (#78)
│ │ ├── NavTabs.tsx Header section tabs Tasks / Time / Journal, active by pathname -
│ │ │ the first cross-section nav available on every page (v3.0/#79)
│ │ ├── journal/ The /journal page (v3.0/#79): JournalClient (orchestrator: date,
│ │ │ contents, debounced autosave), note sections (Checkins/Prose/
│ │ │ Projects/Plan/Evening/List + shared SectionCard), rail widgets
│ │ │ (Calendar, MoodArc, Mind x2, TodaySoFar), FeelingsWheelModal
│ │ │ (drill-down wheel #85: one level per screen, pick logs +
│ │ │ resets to cores, hints under names + in center, ⓘ opens
│ │ │ MocLadder - log-scaled Hawkins reference with today marker),
│ │ │ (3-ring SVG picker, derived MoC), JournalPreview (react-markdown)
│ │ (list is representative - other components: TaskCard, FilterPanel, LabelsClient, etc.)
│ └── lib/
│ ├── api.ts Typed API call functions (used by both server and client)
│ ├── time.ts Pure time-tracking geometry: PX_PER_MIN, daySegment(), dayColumns(),
│ │ dayTotalSeconds(), perTaskTotals(), clockLabel(), dateKey(), addDays() (v2.19.0)
│ ├── quickAdd.ts Pure parseQuickAdd(): NL title -> {deadline, recurrence, project, labels, priority} + token spans (chrono-node for dates; recurrence/tokens hand-rolled) (v2.15.0)
│ ├── deadlinePresets.ts Pure resolvePreset() for the quick-deadline popover (v2.10.2)
│ ├── journal.ts Journal content shapes (per-section-kind contracts, mirrored by
│ │ Services/JournalMarkdown.cs) + derived helpers: moodShift(),
│ │ energyEod(), rolloverCandidates() (v3.0/#79)
│ └── feelingsWheel.ts Curated feelings-wheel dataset: 7 cores / 41 secondaries /
│ (+ per-feeling differentiating `hint` glosses on all 130, #85)
│ 82 tertiaries, each with a Hawkins MoC level + deriveMoc()/
│ mocBand(). Sourced in docs/research/feelings-wheel-moc.md (v3.0/#79)
│
├── docs/
│ ├── architecture.md This file
│ └── plans/ Implementation plans from planning sessions
│
├── CLAUDE.md Instructions for AI assistants
├── LESSONS.md Session learnings log
├── UI-SPEC.md Design tokens and UX rules for v2 frontend
└── Readme.md Human-facing project overview
Runtime: .NET 10 / ASP.NET Core Web API
Database: SQLite via Entity Framework Core 9
Default ports: HTTP 5115, HTTPS 7243 (see launchSettings.json)
HTTP request
│
▼
Controllers Tasks / Projects / Labels / Comments / CheckIns / Habits /
Subtasks / TimeEntries / Journal / MoodCheckins.
Handle routing, validation, HTTP response codes; no business
logic beyond input checking (pure helpers like RecurrenceRule
and HabitStreak live in Services/).
│
▼
TasklogDbContext EF Core context. Direct DbSet access - no repository layer.
│
▼
TasklogDatabase.db SQLite file. Tables: Tasks, Projects, Labels, LabelTaskModel,
Comments, CheckIns, TimeEntries, Subtasks, JournalTemplates,
JournalEntries, MoodCheckins.
Projects
Id INTEGER primary key, autoincrement
Name TEXT not null
Color TEXT nullable ("#RRGGBB" hex string; null = no color. v2.19.0)
CreatedAt TEXT not null (ISO 8601 datetime string)
Tasks
Id INTEGER primary key, autoincrement
Title TEXT not null
Description TEXT nullable (optional free-text notes, <= 2000 chars; null = none) (v2.11.0)
Deadline TEXT nullable (ISO 8601 datetime. Midnight = date-only (due end of day); a non-midnight time = a specific moment. v2.12.0)
CreatedAt TEXT not null (ISO 8601 datetime string)
IsCompleted INTEGER not null default 0 (boolean: 0 = pending, 1 = complete)
CompletedAt TEXT nullable (ISO 8601 datetime string, set when marked complete, cleared on un-complete)
ProjectId INTEGER nullable foreign key -> Projects.Id (null = Inbox)
Priority INTEGER not null default 4 (Todoist P1-P4: 1=urgent .. 4=none; existing rows migrated to 4) (v2.10.5)
Recurrence TEXT nullable (RRULE-shaped rule; null = does not repeat. v2.14.0: daily/every-N/weekly-on-weekdays/monthly-on-day. v2.14.1 adds nth-weekday "BYDAY=3TH", last/from-end "BYMONTHDAY=-1", weekly/monthly INTERVAL>1, and end conditions UNTIL/COUNT)
SeriesId TEXT nullable (Guid linking all occurrences of a repeating task; null for one-offs) (v2.14.0)
IsHabit INTEGER not null default 0 (boolean: 1 = tracked as a daily habit. Existing rows migrate to 0 = false, the CLR zero, so NO HasDefaultValue is needed - contrast Priority's non-zero default) (v2.16.0)
WeeklyTarget INTEGER nullable ("x times a week" habit frequency, 1-7; null = not a frequency habit. A habit is scheduled on specific days (Recurrence) OR by a WeeklyTarget, never both - the controller clears one when the other is set. Nullable int -> existing rows migrate to null, no HasDefaultValue) (v2.18.0)
(response-only) isRecurring bool Recurrence != null. NOT a column; [NotMapped] getter on TaskModel. (v2.14.0)
(response-only) dueStatus string computed from Deadline vs now. A timed deadline goes
"overdue" once its instant passes; a midnight/date-only one stays "today"
all day then overdue next day. NOT a column. [NotMapped] getter on TaskModel,
derived from Deadline relative to DateTime.Today at serialization time.
One of: overdue / today / this_week (through upcoming Sunday) / later / none. (v2.10.3)
Labels
Id INTEGER primary key, autoincrement
Name TEXT not null
ColorIndex INTEGER not null (0-9, maps to VIBGYOR palette in frontend)
CreatedAt TEXT not null (ISO 8601 datetime string)
LabelTaskModel (join table - implicit many-to-many)
LabelsId INTEGER not null foreign key -> Labels.Id (cascade delete)
TasksId INTEGER not null foreign key -> Tasks.Id (cascade delete)
Comments (v2.13.0)
Id INTEGER primary key, autoincrement
Body TEXT not null (free text, <= 2000 chars)
CreatedAt TEXT not null (ISO 8601 datetime string)
TaskId INTEGER not null foreign key -> Tasks.Id (cascade delete)
Subtasks (v2.20.0 - a task's checklist items, #78)
Id INTEGER primary key, autoincrement
Title TEXT not null (<= 500 chars)
IsCompleted INTEGER not null default 0 (boolean)
Position INTEGER not null (manual order within the parent; assigned max+1 on create)
Deadline TEXT nullable (ISO 8601 datetime; shown inline next to the subtask.
Midnight = date-only, mirroring Tasks.Deadline)
CreatedAt TEXT not null (ISO 8601 datetime string)
TaskId INTEGER not null foreign key -> Tasks.Id (cascade delete; indexed)
(task response fields, v2.20.0) subtaskCount / completedSubtaskCount int always present
(drive the "2/5" badge). subtasks[] full rows, [NotMapped] nav - serialized
on GetById, and on GetAll only when includeSubtasks=true (the web loads them
to render the inline checklist; MCP's list_tasks gets counts only).
CheckIns (v2.16.0 - one per habit per day)
Id INTEGER primary key, autoincrement
CheckInDate TEXT not null (date-only, local midnight - the day the habit was done)
CreatedAt TEXT not null (ISO 8601 datetime string)
TaskId INTEGER not null foreign key -> Tasks.Id (cascade delete)
UNIQUE (TaskId, CheckInDate) (makes "done today" idempotent - one row per habit per day)
TimeEntries (v2.19.0 - one interval per start+stop cycle)
Id INTEGER primary key, autoincrement
TaskId INTEGER not null foreign key -> Tasks.Id (cascade delete)
StartedAt TEXT not null (local ISO datetime; no timezone suffix)
EndedAt TEXT nullable (null = currently running; set on stop or next-timer-start)
CreatedAt TEXT not null (ISO 8601 datetime string)
(response-only) DurationSeconds int (EndedAt - StartedAt) in seconds; 0 while running. NOT a column.
(response-only) TaskTitle string denormalized from Task.Title. NOT a column.
(response-only) ProjectId int? denormalized from Task.ProjectId. NOT a column.
(response-only) ProjectColor string? denormalized from Task.Project.Color. NOT a column.
Single-timer invariant: at most one row has EndedAt == null at any time. POST /start auto-stops
any running entry before opening the new one (StopAllRunning helper).
JournalTemplates (v3.0/#79 - a journal note type: Daily, Gratitude, Affirmations)
Id INTEGER primary key, autoincrement
Key TEXT not null, UNIQUE ("daily" / "gratitude" / "affirmations")
Name TEXT not null
Periodicity TEXT not null ("daily"; leaves room for weekly later)
SectionsJson TEXT not null (ordered section defs as a JSON array of
{ key, title, kind, optional? }; kind: checkins / prose /
projects / plan / mind / evening / list. First JSON-as-TEXT
columns in the codebase - opaque to SQL by design.)
SortOrder INTEGER not null (display order)
CreatedAt TEXT not null
Definitions live in code (Services/JournalTemplates.cs) and are UPSERTED by Key at
startup in Program.cs - editing a definition updates the row on next run, no migration.
JournalEntries (v3.0/#79 - one template filled in for one calendar day)
Id INTEGER primary key, autoincrement
TemplateId INTEGER not null foreign key -> JournalTemplates.Id (cascade delete)
EntryDate TEXT not null (date-only, local midnight - same convention as CheckIns)
ContentJson TEXT not null (JSON object keyed by section key; value shape per
section kind - the client contract is frontend/src/lib/journal.ts
and the renderer mirror is Services/JournalMarkdown.cs)
CreatedAt TEXT not null
UpdatedAt TEXT not null
UNIQUE (TemplateId, EntryDate) (one note per template per day - the API upserts,
never duplicates)
MoodCheckins (v3.0/#79 - timestamped mood check-ins, several per day)
Id INTEGER primary key, autoincrement
CheckinAt TEXT not null, indexed (local ISO datetime, TimeEntry convention)
WordsJson TEXT not null (the user's mood words as a JSON string array)
Energy INTEGER not null (0-10)
MocLevel INTEGER nullable (Map of Consciousness level, DERIVED client-side from
feelings-wheel picks - never self-tagged; null = free words only)
CreatedAt TEXT not null
First table with no Task FK - mood belongs to the day, not to a task.
| Method | Path | Description |
|---|---|---|
| GET | /api/tasks |
Filtered/sorted task list. Filter params: projectIds (repeated key), inbox, labelIds (repeated key), dueBefore, dueAfter, createdAfter, createdBefore, completed, text, priorities (repeated key, P1-P4). Sort: sort (created/deadline/priority, default created) + order (asc/desc, default desc; deadline sorts nulls-last, priority asc = P1 first). limit caps to the first N after sorting (<1 → 400). Arrays use repeated keys, not comma-separated. AND across dimensions, OR within id arrays. No params = all tasks, newest-first. inbox=true + projectIds → 400. includeSubtasks=true (v2.20.0, web-only) loads each task's subtasks[] so the web can render the inline checklist; MCP omits it so list_tasks stays lean (counts only). completedOn=yyyy-MM-dd (v3.0/#79) filters to tasks completed that calendar day - feeds the journal's derived "Unplanned, got done" bucket. |
| GET | /api/tasks/{id} |
Single task by ID, including its comments[] (newest first) and subtasks[] (by Position). 404 if not found |
| GET | /api/subtasks |
Global subtask search across all tasks (absolute route). Query: text (case-insensitive title substring), completed. Each match carries its parent taskId + taskTitle. Backs the MCP find tool so "I finished X" resolves without knowing the parent (v2.20.0) |
| GET | /api/tasks/{taskId}/subtasks |
List a task's subtasks in manual order. 404 if task missing (v2.20.0) |
| POST | /api/tasks/{taskId}/subtasks |
Add a subtask. Body: { title, deadline? } (title <= 500). Position = max+1. 201; 400 bad title; 404 task missing (v2.20.0) |
| PATCH | /api/tasks/{taskId}/subtasks/{id} |
Present-key update of title/deadline(null clears)/isCompleted. 404 if not under that task (v2.20.0) |
| DELETE | /api/tasks/{taskId}/subtasks/{id} |
Delete a subtask under that task. 204; 404 if not found (v2.20.0) |
| POST | /api/tasks/{taskId}/subtasks/reorder |
Rewrite Position from { orderedIds } (must be a permutation of the task's subtask ids). 400 otherwise (v2.20.0) |
| GET | /api/tasks/{taskId}/comments |
List a task's comments, newest first. 404 if task missing |
| POST | /api/tasks/{taskId}/comments |
Add a comment. Body: { body } (non-empty, <= 2000). 201 with the created comment; 400 bad body; 404 task missing |
| DELETE | /api/tasks/{taskId}/comments/{id} |
Delete a comment under that task. 204; 404 if not found |
| POST | /api/tasks |
Create task. Body: { title, deadline?, projectId?, priority?, description?, recurrence?, isHabit?, weeklyTarget? }. priority is 1-4 (default 4 = none); description <= 2000 chars (blank → null); recurrence is an RRULE-shaped rule that stamps a SeriesId - it requires a deadline UNLESS the task isHabit (a habit schedules itself with no anchor, v2.18.0); isHabit defaults false; weeklyTarget (1-7) is the "x times a week" habit frequency (habits only, mutually exclusive with recurrence - 400 if both, if non-habit, or out of 1-7). 400 if out of range |
| PATCH | /api/tasks/{id} |
Partial update of title, deadline, priority, description, recurrence, isHabit, and/or weeklyTarget. JSON body, present-key detection: omit=keep, deadline: null/description: null/blank/recurrence: null/weeklyTarget: null=clear, value=set. isHabit is processed first so the effective habit state gates the rest (turning it off keeps past check-ins but clears WeeklyTarget). Setting recurrence requires a deadline UNLESS the task is a habit (v2.18.0), assigns a SeriesId, and clears WeeklyTarget; clearing recurrence nulls Recurrence + SeriesId. Setting weeklyTarget (1-7, habits only) clears Recurrence + SeriesId. Recurrence and weeklyTarget are mutually exclusive - sending both string+number in one PATCH is 400. priority must be 1-4; description <= 2000. 400 on empty title / bad date / bad priority / too-long description / unsupported recurrence / non-boolean isHabit / weeklyTarget out of 1-7 or on a non-habit. Returns the updated task |
| DELETE | /api/tasks/{id} |
Delete task. 204 on success, 404 if not found |
| PATCH | /api/tasks/{id}/complete |
Mark task complete or incomplete. Body: { isCompleted: bool }. Returns the (completed) task. For a recurring task, completing it also spawns the next occurrence (deadline advanced per the rule; title/project/labels/priority/description/recurrence carried under the same SeriesId) and logs a completion comment on the finished one - UNLESS an end condition is reached (v2.14.1: UNTIL date passed or COUNT occurrences exist), in which case the series stops and a "series complete" comment is logged instead. COUNT is evaluated by counting rows with the same SeriesId. Bulk-complete does not spawn. Body may include subtaskMode (v2.20.0): when completing a parent with open subtasks, "completeAll" (default) ticks them all, "pullOut" graduates each open subtask into a standalone task in the parent's project (with a back-reference comment) and detaches it. A recurring occurrence spawns the next one with the subtask checklist reset to unchecked (title + order carried, deadlines dropped) |
| PATCH | /api/tasks/{id}/project |
Reassign task to a project or Inbox. Body: { projectId: int?, projectName?: string }. projectName is resolved by name (case-insensitive, exact) and wins over projectId; 0/multiple matches → 400 |
| POST | /api/tasks/bulk |
Apply one operation to many tasks in one transaction. Body: { operation: "complete" | "assignProject" | "setDeadline" | "setPriority", taskIds: int[], data?: { isCompleted?, projectId?, projectName?, deadline?, priority? } }. assignProject accepts a project name (resolved, wins over id). No bulk delete. Unknown ids skipped; returns the affected tasks. 400 on empty ids / unknown op / invalid data (missing/ambiguous project name, priority out of 1-4) |
| GET | /api/projects |
All projects, ordered by name |
| POST | /api/projects |
Create project. Body: { name: string, color?: string } (color = optional #RRGGBB hex). Returns created project (v2.19.0 adds color) |
| PATCH | /api/projects/{id} |
Rename/recolor project. Body: { name: string, color?: string } (color omitted = unchanged). Returns updated project (v2.19.0 adds color) |
| DELETE | /api/projects/{id} |
Delete project and cascade delete all its tasks. 204 on success |
| GET | /api/time-entries |
Time entries overlapping [from, to) window. Query: from + to (local ISO datetimes, no zone). Defaults to today. Max 366-day range. Entries started before from but still running at from are included. (v2.19.0) |
| GET | /api/time-entries/active |
Currently running entry, or 204 No Content when idle. (v2.19.0) |
| POST | /api/time-entries/start |
Start a timer on a task. Body: { taskId }. Auto-stops any running entry first. 404 if task not found. (v2.19.0) |
| POST | /api/time-entries/{id}/stop |
Stop a running entry. Idempotent: already-stopped entry returned unchanged. (v2.19.0) |
| POST | /api/time-entries |
Manually log a closed interval. Body: { taskId, startedAt, endedAt } (local ISO, no zone). 400 if end <= start or end > now+5min. (v2.19.0) |
| PATCH | /api/time-entries/{id} |
Edit a closed entry's bounds. Body: { startedAt?, endedAt? } (present-key). 400 if end <= start. (v2.19.0) |
| DELETE | /api/time-entries/{id} |
Delete a logged entry. 204 on success. (v2.19.0) |
| GET | /api/labels |
All labels, ordered by name |
| POST | /api/labels |
Create label. Body: { name, colorIndex }. Returns created label |
| PATCH | /api/labels/{id} |
Update label name and/or color. Body: { name, colorIndex }. Returns updated label |
| DELETE | /api/labels/{id} |
Delete label. Unlinks from all tasks (does not delete tasks). 204 on success |
| PATCH | /api/tasks/{id}/labels |
Replace task's label set. Body: { labelIds?: int[], labelNames?: string[] }. labelNames is resolved by name and wins over labelIds; 0/multiple matches → 400. Empty/absent both clear. Returns updated task |
| GET | /api/journal/templates |
All journal templates in display order, section definitions parsed (v3.0/#79) |
| GET | /api/journal/entries?date= |
The day's entries across all templates (empty array = blank day; reads never auto-create). Default today (v3.0/#79) |
| GET | /api/journal/entries/dates?from=&to= |
Days in the range having at least one entry - the calendar's dots. Max 400 days; defaults to the current month (v3.0/#79) |
| PUT | /api/journal/entries/{templateKey}/{date} |
Upsert the day's note for a template. Body: { content: { ... } } (JSON object keyed by section key). 200 with the stored entry; 404 unknown template; 400 non-object content. Never duplicates (unique index) (v3.0/#79) |
| GET | /api/journal/export?date= |
The day's full note (all templates) rendered to markdown, as a yyyy-MM-dd.md download. The preview pane fetches this same output (v3.0/#79) |
| GET | /api/journal/export/all |
Every entry day as one .md each, zipped (journal-export.zip) (v3.0/#79) |
| GET | /api/mood-checkins?date= |
That day's mood check-ins, oldest first (the arc reads left to right). Default today (v3.0/#79) |
| POST | /api/mood-checkins |
Log a check-in. Body: { words: string[], energy: 0-10, mocLevel?, checkinAt? } (checkinAt defaults to now; mocLevel 20-1000). 400 on empty words / out-of-range values (v3.0/#79) |
| DELETE | /api/mood-checkins/{id} |
Remove a mistaken check-in. 204; 404 if not found (v3.0/#79) |
| GET | /api/tasks/{taskId}/checkins |
List a habit's check-in dates, newest first. 404 if task missing (v2.16.0) |
| POST | /api/tasks/{taskId}/checkins |
Log a check-in. Body: { date? } (default today, reduced to date-only). Idempotent: existing day → 200 with that check-in; new day → 201. 404 if task missing (v2.16.0) |
| DELETE | /api/tasks/{taskId}/checkins |
Undo a check-in. Query ?date=yyyy-MM-dd (default today). 204 on success; 404 if there was no check-in that day (v2.16.0) |
| GET | /api/habits |
Habit dashboard: every task where IsHabit, each as { task, currentStreak, doneToday, recentCheckIns[], weeklyTarget, thisWeekCount, recentWeeks[] } (last ~90 days of check-ins, newest-first). currentStreak is unit-aware: consecutive days for a specific-days/daily habit (grace through yesterday), consecutive weeks with >= 1 check-in for a frequency habit (weeklyTarget != null, v2.18.0). For frequency habits, thisWeekCount = days done this calendar week (Mon-Sun) and recentWeeks[] = the last 8 weeks { weekStart, count, status } (status met/partial/none); these three fields are null for non-frequency habits. Ordered newest-created first (v2.16.0) |
Enabled in Development mode only (Program.cs). Allows http://localhost:3000.
In production, a reverse proxy on the same host is assumed - no CORS needed.
Known issue: see GitHub issue #1 - LAN access currently breaks without this fix.
Runtime: Node.js / Next.js 16 (App Router)
Styling: Tailwind CSS v4
Fonts: Space Grotesk (headings), DM Sans (body) via next/font/google
Default port: 3000
Next.js uses file-based routing. Every page.tsx file maps to a URL.
Components are either Server Components (run on the server, no interactivity)
or Client Components (run in the browser, marked with "use client").
src/app/
layout.tsx Server Component. Runs on every request.
Loads fonts, renders header, wraps all pages in <main>.
page.tsx Server Component. Route: /
Renders <ProjectLayout />.
tasks/[id]/
page.tsx Server Component. Route: /tasks/:id
Fetches task and projects from API, renders detail card.
Returns 404 if task not found. Projects fallback to [] silently.
labels/
page.tsx Server Component. Route: /labels. Renders <LabelsClient /> (client-fetched).
habits/
page.tsx Server Component. Route: /habits. Renders <HabitsClient /> (client-fetched). (v2.16.0)
time/
page.tsx Server Component. Route: /time. Renders <TimelineView /> wrapped in
TimeTrackingProvider. (v2.19.0)
journal/
page.tsx Server Component. Route: /journal. Renders <JournalClient />
(client-fetched; owns date, entries, check-ins). (v3.0/#79)
TimeTrackingContext (v2.19.0) - React context provider mounted at app root in layout.tsx.
Holds the single running TimeEntry (or null) and a nowMs that ticks at 1 s ONLY while active,
so elapsed seconds are live across TrackingBar, TimerControl, and TimelineView without a separate
polling loop. Rehydrates from GET /api/time-entries/active on mount.
ProjectLayout.tsx Client Component.
- Owns activeView state ("all" | "inbox" | projectId)
- Fetches projects on mount
- Handles create/rename/delete project actions
- Shows error feedback banner for project operation failures
- Renders ProjectSidebar and TasksClient side by side
- On mobile: renders sidebar as a slide-in drawer
ProjectSidebar.tsx Client Component.
- Renders "All Tasks", "Inbox", and project list
- Highlights the active selection
- Create project: inline input at the bottom
- Rename project: opens an Edit Project modal dialog
- Delete project: opens a confirmation dialog (warns about cascade)
- Delegates all data operations to ProjectLayout via callbacks
TasksClient.tsx Client Component.
- Owns the task list state
- Fetches all tasks on mount (useEffect)
- Filters tasks client-side by activeView prop
- Handles add, delete, and completion operations
- Shows loading spinner, inline feedback messages
- Renders task table with AddTaskForm below
- Project column shown only in "all" view
- Toggle to show/hide completed tasks
AddTaskForm.tsx Client Component.
- Owns title, deadline, and project dropdown state
- Project dropdown pre-selected to active project view
- Syncs dropdown when defaultProjectId prop changes
- Validates title not empty before calling parent's onAdd
- Shows inline field-level error messages
DeleteTaskButton.tsx Client Component.
- Used only on the task detail page
- Calls DELETE API, then redirects to home on success
- Shows spinner during request, error on failure
CompleteTaskButton.tsx Client Component.
- Used only on the task detail page
- Toggles IsCompleted via PATCH API, then calls router.refresh()
- Shows "Mark complete" or "Mark incomplete" based on current state
- Shows spinner during request, error on failure
AssignProjectButton.tsx Client Component.
- Used only on the task detail page
- Dropdown to reassign task to a project or Inbox
- Calls PATCH /api/tasks/{id}/project, then router.refresh()
- State only updates after API confirms (no optimistic update)
LabelChip.tsx Client Component.
- Shared colored pill chip for label display
- Optional onRemove callback renders an × button
- Background color derived from label.colorIndex via labelColor()
AssignLabelsButton.tsx Client Component.
- Used only on the task detail page
- Shows current labels as LabelChip components with remove buttons
- Select dropdown to add unassigned labels
- Calls PATCH /api/tasks/{id}/labels on each change, then router.refresh()
LabelsClient.tsx Client Component.
- Used on the /labels page
- Full CRUD: fetch, create, inline rename, color picker, delete
- Desktop: table layout. Mobile: card list.
ColorPickerButton.tsx Client Component. (v2.19.0 - replaces ColorPicker.tsx)
- Compact swatch button that opens a floating ProjectColorPicker popover
- Closes on Escape, outside-click, or palette selection
- Used by LabelsClient, ProjectSidebar, and TimelineView settings
LabelColorButton.tsx Client Component. (v2.19.0)
- Same popover pattern but for colorIndex (0-9 label palette)
- Used by LabelsClient in table rows and mobile cards
TimelineView.tsx Client Component. (v2.19.0)
- Toggl-style vertical hour grid (00:00-23:00) with day columns
- Day / week toggle; date nav with a date-jump picker
- Entry blocks absolutely positioned by start/duration; project-colored
- Click empty slot -> add popover; click block -> edit/delete popover
- Snap-to-5-min / exact setting (localStorage); Inbox color picker
- Running entry grows live off the TimeTrackingContext 1 s tick
- Per-task totals summary below the grid
TrackingBar.tsx Client Component. (v2.19.0)
- Persistent bottom-left pill on desktop, full-width on mobile
- Idle: "What are you working on?" input + Start button
- Running: task title + live H:MM:SS + Stop button
- Quick-start: creates an Inbox task then starts its timer in one step
TimerControl.tsx Client Component. (v2.19.0)
- Per-task start/stop button on each task row
- Driven by TimeTrackingContext.isRunning(taskId)
FilterPanel.tsx Client Component.
- Popover opened from the task list header three-dot button
- Filter sections: Labels (multi-select chips), Project (checkboxes), Date (radio)
- Apply and Clear buttons; draft state committed only on Apply
All fetch calls go through src/lib/api.ts. This is the only place
that knows the API base URL and constructs request shapes.
getTasks() GET /api/tasks Used by TasksClient (client-side)
getTask(id) GET /api/tasks/:id Used by tasks/[id]/page.tsx (server-side)
createTask() POST /api/tasks Used by TasksClient via AddTaskForm callback
deleteTask(id) DELETE /api/tasks/:id Used by TasksClient and DeleteTaskButton
completeTask(id, bool) PATCH /api/tasks/:id/complete Used by TasksClient and CompleteTaskButton
assignTaskProject(id, pid) PATCH /api/tasks/:id/project Used by AssignProjectButton
getProjects() GET /api/projects Used by ProjectLayout and tasks/[id]/page.tsx
createProject(name) POST /api/projects Used by ProjectLayout
renameProject(id, name) PATCH /api/projects/:id Used by ProjectLayout
deleteProject(id) DELETE /api/projects/:id Used by ProjectLayout
getHabits() GET /api/habits Used by HabitsClient (v2.16.0)
addCheckIn(id, date?) POST /api/tasks/:id/checkins Used by HabitsClient (done-today toggle) (v2.16.0)
removeCheckIn(id, date?) DELETE /api/tasks/:id/checkins Used by HabitsClient (undo) (v2.16.0)
getTimeEntries(from, to) GET /api/time-entries Used by TimelineView (v2.19.0)
getActiveTimeEntry() GET /api/time-entries/active Used by TimeTrackingContext on mount (v2.19.0)
startTimer(taskId) POST /api/time-entries/start Used by TimeTrackingContext (v2.19.0)
stopTimer(id) POST /api/time-entries/:id/stop Used by TimeTrackingContext (v2.19.0)
addTimeEntry(...) POST /api/time-entries Used by TimelineView (manual log) (v2.19.0)
updateTimeEntry(id, ...) PATCH /api/time-entries/:id Used by TimelineView (edit) (v2.19.0)
deleteTimeEntry(id) DELETE /api/time-entries/:id Used by TimelineView (delete) (v2.19.0)
getJournalTemplates() GET /api/journal/templates Used by JournalClient (v3.0)
getJournalEntries(date) GET /api/journal/entries Used by JournalClient (day + yesterday) (v3.0)
getJournalEntryDates(f,t) GET /api/journal/entries/dates Used by JournalClient (calendar dots) (v3.0)
upsertJournalEntry(...) PUT /api/journal/entries/:k/:d Used by JournalClient (debounced autosave) (v3.0)
getMoodCheckins(date) GET /api/mood-checkins Used by JournalClient (v3.0)
addMoodCheckin(...) POST /api/mood-checkins Used by FeelingsWheelModal save (v3.0)
deleteMoodCheckin(id) DELETE /api/mood-checkins/:id Used by CheckinsSection (v3.0)
getJournalDayMarkdown(d) GET /api/journal/export?date= Used by JournalPreview (v3.0)
journalExportUrl(date?) (URL builder) Header download link / zip-all (v3.0)
getTasksCompletedOn(d) GET /api/tasks?completedOn= Used by JournalClient (Unplanned bucket) (v3.0)
searchOpenTasks(text) GET /api/tasks?text=&completed=false Server-side type-ahead (currently unused - the plan combobox filters the loaded list locally) (v3.0)
Known issue: getTask() uses NEXT_PUBLIC_API_URL which resolves to localhost
from a server-side Node.js process. Breaks in production. See GitHub issue #1.
NEXT_PUBLIC_API_URL Base URL for API calls. Defined in frontend/.env.local.
Currently: http://localhost:5115
NEXT_PUBLIC_ prefix = available in both browser and server code.
Runtime: Node.js 20+ via tsx in dev, compiled JS via tsc in production
HTTP framework: Hono with @hono/node-server
MCP SDK: @modelcontextprotocol/sdk (Streamable HTTP transport, stateful mode)
OAuth: hand-rolled, conforming to OAuth 2.1 + RFC 7591/7636/8414/8707/9728
Default port: 5180
Public URL (production): https://mcp-tasklog.manudubey.in via Cloudflare Tunnel
GET / health/identity JSON
POST /mcp JSON-RPC (initialize, tools/*, etc.)
GET /mcp 405 Method Not Allowed (we do not push)
GET /.well-known/oauth-protected-resource RFC 9728 metadata
GET /.well-known/oauth-authorization-server RFC 8414 metadata
POST /register RFC 7591 Dynamic Client Registration
GET /authorize OAuth user-consent page (renders HTML)
GET /auth/github/callback GitHub OAuth upstream callback
POST /token auth_code and refresh_token grants
| Middleware | Purpose | Failure response |
|---|---|---|
originMiddleware |
Origin header allow-list (claude.ai, localhost in dev) | 403 |
protocolVersionMiddleware |
MCP-Protocol-Version: 2025-06-18 if present |
400 |
bearerAuthMiddleware |
Validate access token, check audience claim | 401 + RFC 9728 WWW-Authenticate |
36 MCP tools across five families (tasks: 19, subtasks: 6, projects: 4, labels: 4, time: 7). The subtask family (v2.20.0) - add_subtask, list_subtasks, set_subtask_completion, update_subtask, delete_subtask, reorder_subtasks - wraps the /api/tasks/{taskId}/subtasks sub-resource (registered from tools/subtasks.ts). The task family adds find (v2.20.0): a single by-name search over BOTH tasks and subtasks that returns each hit tagged type: "task" | "subtask" (subtasks carry parentTaskId/parentTitle), so the LLM resolves "I finished X" in one call without knowing whether X is a task or a checklist item; it merges list_tasks(text) with the /api/subtasks search. list_tasks stays for structured filtering (project/label/date/priority). The task family includes four bulk tools (bulk_set_completion, bulk_assign_to_project, bulk_set_deadline, bulk_set_priority) backed by the single POST /api/tasks/bulk endpoint; add_task_comment, list_task_comments, delete_task_comment (v2.19.0); log_habit_checkin, undo_habit_checkin, get_habit_checkins (v2.19.0); and get_habits (full habits dashboard with streak, done-today, weekly progress; v2.19.0). assign_task_to_project / bulk_assign_to_project / set_task_labels accept a name as an alternative to an id (resolved server-side). create_task and update_task accept an RRULE-shaped recurrence string (v2.14.0), an isHabit flag (v2.16.0), and a weeklyTarget (1-7) for "x times a week" habits (v2.18.0). create_project / rename_project accept an optional color hex (v2.19.0). The time family (v2.19.0) covers start_timer, stop_timer, get_active_timer, log_time, edit_time_entry, delete_time_entry, and get_time_summary (totals by task for a date range). Each tool is a thin wrapper around the corresponding Tasklog /api endpoint via api-client.ts. Input schemas use Zod and are inlined per tool. The runTool() helper in result.ts converts thrown ApiErrors into MCP isError: true tool results (not JSON-RPC protocol errors), so the LLM can see and react to failures.
The list_tasks tool accepts an optional filter object (project, inbox, labels, deadline range, completion, title substring) that api-client.ts serializes into a query string on GET /api/tasks. Completion is a single set_task_completion(id, isCompleted) tool - the earlier complete_task / uncomplete_task split was consolidated in v2.10.1.
Separate SQLite file at mcp/data/auth.db (not the Tasklog DB). Four tables:
clients
client_id TEXT primary key (opaque, generated by /register)
client_name TEXT
redirect_uris TEXT JSON-encoded array
created_at INTEGER unix epoch seconds
auth_codes
code TEXT primary key (one-use, consumed by /token)
client_id TEXT
redirect_uri TEXT
code_challenge TEXT S256 base64url
code_challenge_method TEXT
scope TEXT
resource TEXT RFC 8707 resource indicator
github_user TEXT verified GitHub login
expires_at INTEGER
access_tokens
token TEXT primary key (opaque, 32-byte hex)
client_id TEXT
audience TEXT must match MCP_PUBLIC_URL on validation
github_user TEXT
scope TEXT
expires_at INTEGER TTL 1h
refresh_tokens
same shape as access_tokens, TTL 30d, rotated on every /token use
consume() queries on auth_codes and refresh_tokens are transactional read+delete so replay attacks fail.
claude.ai opens /authorize in browser
-> we validate params, set signed flow cookie, render "Log in with GitHub"
-> user clicks, goes to GitHub
-> GitHub redirects to /auth/github/callback
-> we exchange code with GitHub, fetch user identity
-> we check login against ALLOWED_GH_USERS env var
-> we mint our auth code, 302 to claude.ai's callback
claude.ai POSTs /token with code + PKCE verifier
-> we verify PKCE, issue access + refresh tokens
claude.ai POSTs /mcp with Authorization: Bearer <access_token>
-> middleware validates token + audience
-> MCP transport handles tools/list, tools/call
See docs/learnings/oauth-2-1-for-mcp.md for the full mechanics, docs/learnings/mcp-protocol.md for what MCP itself is, and guides/mcp-server-setup.md for the end-to-end setup walkthrough.
| Var | Purpose | Required in prod |
|---|---|---|
PORT |
HTTP listen port (default 5180) | no |
TASKLOG_API_URL |
Where to reach the Tasklog API | no (default localhost:5115) |
MCP_PUBLIC_URL |
Canonical public URL; used as token audience | yes |
GITHUB_CLIENT_ID |
Upstream GitHub OAuth App | yes |
GITHUB_CLIENT_SECRET |
Same; secret | yes |
ALLOWED_GH_USERS |
Comma-separated GitHub login allow-list | yes |
SESSION_SECRET |
HMAC key for signed flow cookies | yes |
NODE_ENV |
production enables strict env validation |
implicit |
AUTH_DB_PATH |
SQLite path (default data/auth.db) | no |
In production, config.ts throws on startup if any required var is missing or still at its dev default. Secrets live in /root/.tasklog-mcp.env (chmod 600) inside proot, sourced by the runit service script.
Opening the home page:
1. Browser requests http://localhost:3000/
2. Next.js renders layout.tsx + page.tsx on server (Server Components)
3. HTML shell sent to browser
4. Browser loads TasksClient.tsx JavaScript
5. TasksClient useEffect fires: GET http://localhost:5115/api/tasks
6. .NET queries SQLite, returns JSON array
7. React renders the task table
Adding a task:
1. User fills form and clicks "Add Task"
2. AddTaskForm validates input, calls onAdd() callback
3. TasksClient calls createTask() in api.ts
4. POST http://localhost:5115/api/tasks with JSON body
5. .NET inserts row in SQLite, returns new task JSON
6. TasksClient prepends task to local state
7. React re-renders the list - no page reload
Viewing a task detail:
1. Browser requests http://localhost:3000/tasks/42
2. Next.js runs tasks/[id]/page.tsx on the server
3. Server calls getTask(42): GET http://localhost:5115/api/tasks/42
4. .NET returns task JSON
5. Server renders HTML with task data
6. Browser receives complete HTML - no client-side fetch needed
Asking Claude to create a task (post v2.10):
1. User types in claude.ai mobile: "add task: review PR by Friday"
2. claude.ai sends prompt to its LLM, which decides to invoke our connector
3. claude.ai opens TLS connection to https://mcp-tasklog.manudubey.in/mcp
4. Cloudflare edge terminates TLS, forwards over the tunnel to phone:5180
5. tasklog-mcp middleware: Origin OK (claude.ai), Bearer token validated, audience matches
6. McpServer routes tools/call -> create_task handler
7. Handler POSTs http://localhost:5115/api/tasks (LAN-only call inside the phone)
8. Tasklog .NET API inserts row in SQLite, returns new task JSON
9. Handler wraps the result in a CallToolResult content block
10. Response flows back: phone -> tunnel -> Cloudflare edge -> claude.ai -> LLM -> user
These are tracked as GitHub issues:
| Issue | Description |
|---|---|
| #1 | CORS and server-side localhost URL break app outside dev |
| #2 | Optimistic delete and feedback timer state bugs |
| #3 | Fragile database path and silent API URL failure |
These are planned but not built:
- Pagination
- Authentication
- Production deployment configuration