diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c69c609..c004d65 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,11 +3,14 @@ {"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, \u0026task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"} {"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} +{"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"} {"id":"checkvist-api-4qn","title":"Fix date parsing for Checkvist API format","description":"## Problem\n\nThe Checkvist API returns timestamps in a non-standard format that Go's `time.Time` cannot parse with default JSON unmarshaling.\n\n**API returns:** `\"2026/01/14 16:07:31 +0000\"`\n**Go expects:** `\"2006-01-02T15:04:05Z07:00\"` (RFC3339/ISO8601)\n\nThis causes errors like:\n```\nparsing time \"2026/01/14 16:07:31 +0000\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"/01/14 16:07:31 +0000\" as \"-\"\n```\n\n## Affected Fields\n\n- `Checklist.UpdatedAt`\n- `Task.UpdatedAt`\n- `Task.CreatedAt`\n- `Note.UpdatedAt`\n- `Note.CreatedAt`\n\n## Solution\n\nCreate a custom `APITime` type that implements `json.Unmarshaler` to handle the Checkvist date format:\n\n```go\ntype APITime struct {\n time.Time\n}\n\nfunc (t *APITime) UnmarshalJSON(data []byte) error {\n // Try multiple formats:\n // 1. Checkvist format: \"2006/01/02 15:04:05 -0700\"\n // 2. ISO8601/RFC3339: \"2006-01-02T15:04:05Z07:00\"\n}\n```\n\nReplace `time.Time` with `APITime` in all model structs.\n\n## Test\n\nA test has been added that documents this bug:\n`TestChecklists_List_RealAPIDateFormat` in `checklists_test.go`\n\nCurrently skips with message showing the parsing error. Should pass after fix.","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T17:55:53.54028308+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T18:09:52.876804767+01:00","closed_at":"2026-01-14T18:09:52.876804767+01:00","close_reason":"APITime type with custom UnmarshalJSON implemented, all tests passing"} {"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"} +{"id":"checkvist-api-6j7","title":"Add WithNote() to TaskBuilder for inline note creation","description":"## Description\n\nCurrently, creating a task and adding a note to it requires two separate steps:\n1. Create the task via `Tasks().Create()`\n2. Add a note to the created task via `Notes().Create()`\n\nThis feature adds a `WithNote()` method to the `TaskBuilder` that allows specifying a note inline during task creation.\n\n## Desired API\n\n```go\ntask, err := client.Tasks(checklistID).Create(ctx,\n checkvist.NewTask(\"Task content\").\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(1).\n WithNote(\"This is a note attached to the task\"),\n)\n```\n\n## Implementation\n\nThe API client should:\n1. First create the task via the existing endpoint\n2. If a note is specified, automatically create the note for the returned task ID\n3. Return the created task (note creation is a side effect)\n\n## Acceptance Criteria\n\n- [ ] `TaskBuilder` has a new `WithNote(string)` method\n- [ ] When `WithNote()` is used, the note is created after the task\n- [ ] If task creation fails, no note is created\n- [ ] If note creation fails, appropriate error handling (task already exists)\n- [ ] Tests cover the combined creation flow\n- [ ] GoDoc examples demonstrate usage","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T08:36:20.184071069+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T08:36:20.184071069+01:00"} {"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:33.200281826+01:00","closed_at":"2026-01-14T14:37:33.200281826+01:00","close_reason":"WithRepeat Methode zu TaskBuilder hinzugefügt mit GoDoc-Dokumentation","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-8q3","title":"Set up Mage build targets","description":"Create magefiles/magefile.go with:\n- Test() - run go test -v ./...\n- Coverage() - run go test -coverprofile=coverage.out ./...\n- Lint() - run staticcheck ./...\n- Fmt() - run gofmt -w .\n- Check() - run all quality checks (fmt, vet, staticcheck, test)\nEnsure magefiles has its own go.mod importing magefile.org/mage","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:09.228450637+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:33:08.511791793+01:00","closed_at":"2026-01-14T13:33:08.511791793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8q3","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:48.556022687+01:00","created_by":"Oliver Jakoubek"}]} @@ -23,6 +26,8 @@ {"id":"checkvist-api-lpn","title":"Implement authentication with auto token renewal","description":"Add to client.go:\n- Authenticate(ctx context.Context) error - explicit login\n- refreshToken(ctx context.Context) error - token renewal\n- ensureAuthenticated(ctx context.Context) error - auto-auth before requests\n- CurrentUser(ctx context.Context) (*User, error) - get logged in user\n- Token management: store token and expiry, auto-refresh before expiry\n- Thread-safe token access using mutex\n- Support for optional 2FA token\nAPI endpoints:\n- POST /auth/login.json?version=2 (login)\n- POST /auth/refresh_token.json?version=2 (refresh)\n- GET /auth/curr_user.json (current user)\nToken sent via X-Client-Token header","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.358878117+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:26:32.668016856+01:00","closed_at":"2026-01-14T13:26:32.668016856+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-lpn","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.656124681+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"} +{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n\u003e \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:42:22.954414951+01:00","closed_at":"2026-01-18T14:42:22.954414951+01:00","close_reason":"Fixed Task.tasks field type: renamed Children []*Task to ChildIDs []int"} {"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} diff --git a/.beads/sync_base.jsonl b/.beads/sync_base.jsonl new file mode 100644 index 0000000..15b99f0 --- /dev/null +++ b/.beads/sync_base.jsonl @@ -0,0 +1,34 @@ +{"id":"checkvist-api-0l6","title":"Implement Checklist archive/unarchive","description":"Add P2 (nice-to-have) archive functionality to ChecklistService:\n- Archive(ctx, id) (*Checklist, error)\n- Unarchive(ctx, id) (*Checklist, error)\nUse PUT /checklists/{id}.json with archived field.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.538852525+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:36:35.386071331+01:00","closed_at":"2026-01-14T14:36:35.386071331+01:00","close_reason":"Archive und Unarchive Methoden implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-0l6","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:02.869871005+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1e6","title":"Project Setup & Foundation","description":"Phase 1: Set up Go module structure, basic client, authentication, and error handling. This is the foundation for all other work.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:05.916898362+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.984811878+01:00","closed_at":"2026-01-14T14:29:26.984811878+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"} +{"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: <10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, &task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"} +{"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-47y","title":"Quality & Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target >80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} +{"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"} +{"id":"checkvist-api-4qn","title":"Fix date parsing for Checkvist API format","description":"## Problem\n\nThe Checkvist API returns timestamps in a non-standard format that Go's `time.Time` cannot parse with default JSON unmarshaling.\n\n**API returns:** `\"2026/01/14 16:07:31 +0000\"`\n**Go expects:** `\"2006-01-02T15:04:05Z07:00\"` (RFC3339/ISO8601)\n\nThis causes errors like:\n```\nparsing time \"2026/01/14 16:07:31 +0000\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"/01/14 16:07:31 +0000\" as \"-\"\n```\n\n## Affected Fields\n\n- `Checklist.UpdatedAt`\n- `Task.UpdatedAt`\n- `Task.CreatedAt`\n- `Note.UpdatedAt`\n- `Note.CreatedAt`\n\n## Solution\n\nCreate a custom `APITime` type that implements `json.Unmarshaler` to handle the Checkvist date format:\n\n```go\ntype APITime struct {\n time.Time\n}\n\nfunc (t *APITime) UnmarshalJSON(data []byte) error {\n // Try multiple formats:\n // 1. Checkvist format: \"2006/01/02 15:04:05 -0700\"\n // 2. ISO8601/RFC3339: \"2006-01-02T15:04:05Z07:00\"\n}\n```\n\nReplace `time.Time` with `APITime` in all model structs.\n\n## Test\n\nA test has been added that documents this bug:\n`TestChecklists_List_RealAPIDateFormat` in `checklists_test.go`\n\nCurrently skips with message showing the parsing error. Should pass after fix.","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T17:55:53.54028308+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T18:09:52.876804767+01:00","closed_at":"2026-01-14T18:09:52.876804767+01:00","close_reason":"APITime type with custom UnmarshalJSON implemented, all tests passing"} +{"id":"checkvist-api-5ab","title":"Implement Note operations","description":"Create notes.go with NoteService:\n- client.Notes(checklistID, taskID) returns NoteService\n- List(ctx) ([]Note, error) - GET /checklists/{id}/tasks/{task_id}/comments.json\n- Create(ctx, comment string) (*Note, error) - POST /checklists/{id}/tasks/{task_id}/comments.json\n- Update(ctx, noteID, comment string) (*Note, error) - PUT /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\n- Delete(ctx, noteID) error - DELETE /checklists/{id}/tasks/{task_id}/comments/{note_id}.json\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:54.268124634+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:40:31.06124818+01:00","closed_at":"2026-01-14T13:40:31.06124818+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-5ab","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:32:55.843507717+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-5wr","title":"Initialize Go module and project structure","description":"Create go.mod with module path code.beautifulmachines.dev/jakoubek/checkvist-api. Set up flat package structure with placeholder files: client.go, checklists.go, tasks.go, notes.go, filter.go, models.go, errors.go, options.go. Create magefiles/ directory with separate go.mod for Mage build targets. Add LICENSE (MIT) file.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.285510329+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T12:43:45.392753825+01:00","closed_at":"2026-01-14T12:43:45.392753825+01:00","close_reason":"Closed"} +{"id":"checkvist-api-6j7","title":"Add WithNote() to TaskBuilder for inline note creation","description":"## Description\n\nCurrently, creating a task and adding a note to it requires two separate steps:\n1. Create the task via `Tasks().Create()`\n2. Add a note to the created task via `Notes().Create()`\n\nThis feature adds a `WithNote()` method to the `TaskBuilder` that allows specifying a note inline during task creation.\n\n## Desired API\n\n```go\ntask, err := client.Tasks(checklistID).Create(ctx,\n checkvist.NewTask(\"Task content\").\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(1).\n WithNote(\"This is a note attached to the task\"),\n)\n```\n\n## Implementation\n\nThe API client should:\n1. First create the task via the existing endpoint\n2. If a note is specified, automatically create the note for the returned task ID\n3. Return the created task (note creation is a side effect)\n\n## Acceptance Criteria\n\n- [ ] `TaskBuilder` has a new `WithNote(string)` method\n- [ ] When `WithNote()` is used, the note is created after the task\n- [ ] If task creation fails, no note is created\n- [ ] If note creation fails, appropriate error handling (task already exists)\n- [ ] Tests cover the combined creation flow\n- [ ] GoDoc examples demonstrate usage","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T08:36:20.184071069+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T08:36:20.184071069+01:00"} +{"id":"checkvist-api-8bn","title":"Write unit tests for Client and Auth","description":"Create client_test.go with tests using httptest.Server:\n- TestNewClient_Defaults\n- TestNewClient_WithOptions\n- TestAuthenticate_Success\n- TestAuthenticate_InvalidCredentials\n- TestAuthenticate_2FA\n- TestTokenRefresh_Auto\n- TestTokenRefresh_Manual\n- TestCurrentUser\n- TestRetryLogic_429\n- TestRetryLogic_5xx\n- TestRetryLogic_NetworkError\nUse table-driven tests. Create testdata/auth/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.964610587+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:35:19.981723023+01:00","closed_at":"2026-01-14T13:35:19.981723023+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:33:12.783142853+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8bn","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:33:13.232028837+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-8jh","title":"Implement repeating tasks configuration","description":"Add P2 (nice-to-have) repeat support to TaskBuilder:\n- WithRepeat(pattern string) *TaskBuilder\nSupport Checkvist smart syntax for repeats.\nDocument common patterns in GoDoc.","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.826106108+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:33.200281826+01:00","closed_at":"2026-01-14T14:37:33.200281826+01:00","close_reason":"WithRepeat Methode zu TaskBuilder hinzugefügt mit GoDoc-Dokumentation","dependencies":[{"issue_id":"checkvist-api-8jh","depends_on_id":"checkvist-api-tjk","type":"blocks","created_at":"2026-01-14T12:33:03.159849575+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-8q3","title":"Set up Mage build targets","description":"Create magefiles/magefile.go with:\n- Test() - run go test -v ./...\n- Coverage() - run go test -coverprofile=coverage.out ./...\n- Lint() - run staticcheck ./...\n- Fmt() - run gofmt -w .\n- Check() - run all quality checks (fmt, vet, staticcheck, test)\nEnsure magefiles has its own go.mod importing magefile.org/mage","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:09.228450637+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:33:08.511791793+01:00","closed_at":"2026-01-14T13:33:08.511791793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8q3","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:48.556022687+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-8u6","title":"Implement HTTP request helper with retry logic","description":"Add internal HTTP helper to client.go:\n- doRequest(ctx, method, path, body) helper for all API calls\n- Automatic authentication check before requests\n- JSON marshaling/unmarshaling\n- Exponential backoff retry for:\n - HTTP 429 (Too Many Requests)\n - HTTP 5xx (Server Errors)\n - Network errors (timeout, connection reset)\n- Respect context cancellation\n- Optional debug logging of requests/responses via slog","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.780244392+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:27:52.914675409+01:00","closed_at":"2026-01-14T13:27:52.914675409+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.973194416+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-8u6","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:48.268500727+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-93m","title":"Create CHANGELOG","description":"Create CHANGELOG.md following Keep a Changelog format:\n- [Unreleased] section for ongoing work\n- Initial release preparation notes\n- Document all features implemented","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:39.009748936+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:37:00.644317886+01:00","closed_at":"2026-01-14T13:37:00.644317886+01:00","close_reason":"Closed"} +{"id":"checkvist-api-a5b","title":"Fix TaskBuilder parameters not sent to API","description":"## Problem\n\nWhen creating tasks with `TaskBuilder`, the task is created but additional parameters (due date, priority, tags) are not applied. Only the task content is saved.\n\n## Reproduction\n\n```go\ntask, err := client.Tasks(checklist.ID).Create(ctx,\n checkvist.NewTask(\"Test-Task\").\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(1).\n WithTags(\"OLI\", \"Test\"),\n)\n// Task is created, but due date, priority, and tags are missing\n```\n\n## Likely Root Cause\n\nSimilar to the Notes API issue (checkvist-api-awg), the Checkvist API likely expects nested parameters in the format `task[content]`, `task[due]`, etc. instead of flat JSON.\n\n**Current (likely incorrect):**\n```json\n{\"content\": \"text\", \"due\": \"^tomorrow\", \"priority\": 1, \"tags\": \"OLI, Test\"}\n```\n\n**Expected by API (likely):**\n```json\n{\"task\": {\"content\": \"text\", \"due\": \"^tomorrow\", \"priority\": 1, \"tags\": \"OLI, Test\"}}\n```\n\n## Affected Code\n\n`tasks.go`:\n- `CreateTaskRequest` struct (lines 55-64)\n- `build()` method (lines 127-146)\n- `Create()` method (lines 148-159)\n\n## Solution\n\nWrap `CreateTaskRequest` in a `task` field similar to how we fixed Notes:\n\n```go\ntype taskWrapper struct {\n Task CreateTaskRequest `json:\"task\"`\n}\n```\n\n## Acceptance Criteria\n\n- Task created with `WithDueDate()` has due date set\n- Task created with `WithPriority()` has priority set\n- Task created with `WithTags()` has tags set\n- All combinations work together","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T18:20:41.761840004+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T19:02:24.902832354+01:00","closed_at":"2026-01-14T19:02:24.902832354+01:00","close_reason":"Closed"} +{"id":"checkvist-api-awg","title":"Fix Notes.Create API parameter format","description":"## Problem\n\nThe `NoteService.Create` method fails with a 400 Bad Request error:\n\n```\ncheckvist API error (status 400): {\"message\":\"comment[comment] parameter is required\"}\n```\n\n## Root Cause\n\nThe Checkvist API expects nested parameters in the format `comment[comment]`, but the current implementation sends a flat JSON structure:\n\n**Current (incorrect):**\n```json\n{\"comment\": \"Note text\"}\n```\n\n**Expected by API:**\n```json\n{\"comment\": {\"comment\": \"Note text\"}}\n```\n\n## Affected Code\n\n`notes.go` lines 37-51:\n```go\ntype createNoteRequest struct {\n Comment string `json:\"comment\"`\n}\n\nfunc (s *NoteService) Create(ctx context.Context, comment string) (*Note, error) {\n path := fmt.Sprintf(\"/checklists/%d/tasks/%d/comments.json\", s.checklistID, s.taskID)\n body := createNoteRequest{Comment: comment}\n // ...\n}\n```\n\n## Solution\n\nChange `createNoteRequest` to use nested structure:\n\n```go\ntype createNoteRequest struct {\n Comment struct {\n Comment string `json:\"comment\"`\n } `json:\"comment\"`\n}\n```\n\nOr create a wrapper struct for clarity.\n\n## Likely Affected Methods\n\n- `NoteService.Create` - confirmed broken\n- `NoteService.Update` - likely same issue (uses `updateNoteRequest`)\n\n## Reproduction\n\n```go\nnote, err := client.Notes(checklistID, taskID).Create(ctx, \"Test note\")\n// Returns: API error 400, \"comment[comment] parameter is required\"\n```","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T18:12:27.03448075+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T18:18:20.446447325+01:00","closed_at":"2026-01-14T18:18:20.446447325+01:00","close_reason":"Fixed nested JSON parameter format for Create and Update methods"} +{"id":"checkvist-api-bbx","title":"Write unit tests for Notes","description":"Create notes_test.go with tests:\n- TestNotes_List\n- TestNotes_Create\n- TestNotes_Update\n- TestNotes_Delete\nUse table-driven tests. Create testdata/notes/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.829382141+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:47:17.141162679+01:00","closed_at":"2026-01-14T13:47:17.141162679+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-bbx","depends_on_id":"checkvist-api-5ab","type":"blocks","created_at":"2026-01-14T12:33:14.119755191+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-br3","title":"Core API Operations","description":"Phase 2: Implement CRUD operations for Checklists, Tasks, and Notes. All P0 (must-have) features for the library.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.20627925+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:29:26.991139668+01:00","closed_at":"2026-01-14T14:29:26.991139668+01:00","close_reason":"Alle zugehörigen Tasks abgeschlossen"} +{"id":"checkvist-api-c2k","title":"Implement Checklist operations","description":"Create checklists.go with ChecklistService:\n- client.Checklists() returns ChecklistService\n- List(ctx) ([]Checklist, error) - GET /checklists.json\n- Get(ctx, id) (*Checklist, error) - GET /checklists/{id}.json\n- Create(ctx, name) (*Checklist, error) - POST /checklists.json\n- Update(ctx, id, name) (*Checklist, error) - PUT /checklists/{id}.json\n- Delete(ctx, id) error - DELETE /checklists/{id}.json\n- Support archived filter in List\nContext support for all methods.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.566197933+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:38:06.162666425+01:00","closed_at":"2026-01-14T13:38:06.162666425+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:54.533462004+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-c2k","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:54.859645166+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-cb8","title":"Extended Features","description":"Phase 3: Implement P1 (should-have) features including client-side filtering and builder patterns for fluent interfaces.","status":"closed","priority":1,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.624242123+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.141188241+01:00","closed_at":"2026-01-14T14:37:52.141188241+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} +{"id":"checkvist-api-e9p","title":"Implement data models","description":"Create models.go with all data structures:\n- Checklist struct (ID, Name, Public, Archived, ReadOnly, TaskCount, TaskCompleted, Tags, TagsAsText, UpdatedAt)\n- Task struct (ID, ChecklistID, ParentID, Content, Status, Position, Priority, Tags, TagsAsText, DueDateRaw, DueDate, AssigneeIDs, CommentsCount, UpdateLine, UpdatedAt, CreatedAt, Children, Notes)\n- TaskStatus enum (StatusOpen=0, StatusClosed=1, StatusInvalidated=2)\n- Note struct (ID, TaskID, Comment, UpdatedAt, CreatedAt)\n- Tags type (map[string]bool)\n- User struct (ID, Username, Email)\n- DueDate struct with constructors (DueAt, DueString, DueInDays) and constants (DueToday, DueTomorrow, DueNextWeek, DueNextMonth)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:06.900391036+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:22:22.273934664+01:00","closed_at":"2026-01-14T13:22:22.273934664+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-e9p","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.433908937+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-lpn","title":"Implement authentication with auto token renewal","description":"Add to client.go:\n- Authenticate(ctx context.Context) error - explicit login\n- refreshToken(ctx context.Context) error - token renewal\n- ensureAuthenticated(ctx context.Context) error - auto-auth before requests\n- CurrentUser(ctx context.Context) (*User, error) - get logged in user\n- Token management: store token and expiry, auto-refresh before expiry\n- Thread-safe token access using mutex\n- Support for optional 2FA token\nAPI endpoints:\n- POST /auth/login.json?version=2 (login)\n- POST /auth/refresh_token.json?version=2 (refresh)\n- GET /auth/curr_user.json (current user)\nToken sent via X-Client-Token header","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.358878117+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:26:32.668016856+01:00","closed_at":"2026-01-14T13:26:32.668016856+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-lpn","depends_on_id":"checkvist-api-ymg","type":"blocks","created_at":"2026-01-14T12:32:47.656124681+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = &t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = &t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"} +{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n> \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"in_progress","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:34:44.774842767+01:00"} +{"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-ymg","title":"Implement Client struct with functional options","description":"Create client.go with:\n- Client struct (baseURL, username, remoteKey, token, tokenExp, httpClient, retryConf, logger, mu sync.RWMutex)\n- NewClient(username, remoteKey string, opts ...Option) *Client constructor\n- Create options.go with Option type and functional options:\n - WithHTTPClient(*http.Client)\n - WithTimeout(time.Duration)\n - WithRetryConfig(RetryConfig)\n - WithLogger(*slog.Logger)\n - WithBaseURL(string) for testing\n- RetryConfig struct (MaxRetries, BaseDelay, MaxDelay, Jitter)\n- Default values: 3 retries, 1s base delay, 30s max delay, jitter enabled","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:08.021154076+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:24:55.101093793+01:00","closed_at":"2026-01-14T13:24:55.101093793+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-e9p","type":"blocks","created_at":"2026-01-14T12:32:47.077541448+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-ymg","depends_on_id":"checkvist-api-mnh","type":"blocks","created_at":"2026-01-14T12:32:47.358639632+01:00","created_by":"Oliver Jakoubek"}]} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8215b8d..5a1c7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.3] - 2026-01-18 + +### Changed + +- **BREAKING**: `Task.Children []*Task` renamed to `Task.ChildIDs []int` to match API response format (API returns array of child task IDs, not full task objects) + +### Fixed + +- **Tasks**: Fix `Close()`, `Reopen()`, and `Invalidate()` to handle API array response format + +## [1.0.2] - 2026-01-15 + +### Fixed + +- **DueDate**: Remove caret (`^`) prefix from due date values - API expects values like `"Tomorrow"` not `"^Tomorrow"` + +### Removed + +- **DueDate**: Remove unused `DueASAP` and `DueMonday` constants + +## [1.0.1] - 2026-01-15 + +### Fixed + +- **DueDate**: Fix parsing of due dates returned by API - now supports both slash format (`2026/01/15`) and ISO format (`2026-01-15`) + +## [1.0.0] - 2026-01-14 + ### Added - **Client**: Core API client with functional options pattern diff --git a/client_test.go b/client_test.go index abf85ee..f9beba8 100644 --- a/client_test.go +++ b/client_test.go @@ -458,11 +458,11 @@ func TestCalculateRetryDelay(t *testing.T) { attempt int expected time.Duration }{ - {1, 200 * time.Millisecond}, // 100ms * 2^1 - {2, 400 * time.Millisecond}, // 100ms * 2^2 - {3, 800 * time.Millisecond}, // 100ms * 2^3 - {4, 1 * time.Second}, // capped at MaxDelay - {5, 1 * time.Second}, // capped at MaxDelay + {1, 200 * time.Millisecond}, // 100ms * 2^1 + {2, 400 * time.Millisecond}, // 100ms * 2^2 + {3, 800 * time.Millisecond}, // 100ms * 2^3 + {4, 1 * time.Second}, // capped at MaxDelay + {5, 1 * time.Second}, // capped at MaxDelay } for _, tc := range tests { diff --git a/models.go b/models.go index 42ebb52..f4aaf84 100644 --- a/models.go +++ b/models.go @@ -140,8 +140,8 @@ type Task struct { UpdatedAt APITime `json:"updated_at"` // CreatedAt is the timestamp when the task was created. CreatedAt APITime `json:"created_at"` - // Children contains nested child tasks (when fetched with tree structure). - Children []*Task `json:"tasks,omitempty"` + // ChildIDs contains IDs of child tasks (returned by API as array of integers). + ChildIDs []int `json:"tasks,omitempty"` // Notes contains the comments/notes attached to this task. Notes []Note `json:"notes,omitempty"` } @@ -175,25 +175,21 @@ type DueDate struct { value string } -// Common due date constants using Checkvist's smart syntax. +// Common due date constants for the Checkvist API. var ( // DueToday sets the due date to today. - DueToday = DueDate{value: "^Today"} + DueToday = DueDate{value: "Today"} // DueTomorrow sets the due date to tomorrow. - DueTomorrow = DueDate{value: "^Tomorrow"} - // DueASAP sets the due date to ASAP. - DueASAP = DueDate{value: "^ASAP"} - // DueMonday sets the due date to next Monday. - DueMonday = DueDate{value: "^Monday"} + DueTomorrow = DueDate{value: "Tomorrow"} ) // DueAt creates a DueDate from a Go time.Time value. func DueAt(t time.Time) DueDate { - return DueDate{value: "^" + t.Format("2006-01-02")} + return DueDate{value: t.Format("2006-01-02")} } -// DueString creates a DueDate from a raw smart syntax string. -// The string should use Checkvist's smart syntax (e.g., "^2026-02-01", "^friday", "^next week"). +// DueString creates a DueDate from a raw string. +// Use this for custom date formats (e.g., "2026-02-01", "friday", "next week"). func DueString(s string) DueDate { return DueDate{value: s} } @@ -201,7 +197,7 @@ func DueString(s string) DueDate { // DueInDays creates a DueDate for n days from now. func DueInDays(n int) DueDate { t := time.Now().AddDate(0, 0, n) - return DueDate{value: "^" + t.Format("2006-01-02")} + return DueDate{value: t.Format("2006-01-02")} } // String returns the smart syntax string for the due date. diff --git a/tasks.go b/tasks.go index 9198c7b..f45ad34 100644 --- a/tasks.go +++ b/tasks.go @@ -54,13 +54,13 @@ func (s *TaskService) Get(ctx context.Context, taskID int) (*Task, error) { // CreateTaskRequest represents the request body for creating a task. type CreateTaskRequest struct { - Content string `json:"content"` - ParentID int `json:"parent_id,omitempty"` - Position int `json:"position,omitempty"` - Due string `json:"due_date,omitempty"` - Priority int `json:"priority,omitempty"` - Tags string `json:"tags,omitempty"` - Repeat string `json:"repeat,omitempty"` + Content string `json:"content"` + ParentID int `json:"parent_id,omitempty"` + Position int `json:"position,omitempty"` + Due string `json:"due_date,omitempty"` + Priority int `json:"priority,omitempty"` + Tags string `json:"tags,omitempty"` + Repeat string `json:"repeat,omitempty"` } // createTaskWrapper wraps the task fields for the nested JSON format @@ -201,53 +201,75 @@ func (s *TaskService) Delete(ctx context.Context, taskID int) error { } // Close marks a task as completed. +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Close(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/close.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("close task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // Reopen reopens a closed or invalidated task. +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Reopen(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/reopen.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("reopen task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // Invalidate marks a task as invalidated (strikethrough). +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Invalidate(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/invalidate.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("invalidate task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // parseDueDate attempts to parse the DueDateRaw string into a time.Time. -// It supports ISO 8601 date format (YYYY-MM-DD). +// It supports the Checkvist API format (YYYY/MM/DD) and ISO 8601 format (YYYY-MM-DD). func parseDueDate(task *Task) { if task.DueDateRaw == "" { return } - // Try to parse as ISO date - if t, err := time.Parse("2006-01-02", task.DueDateRaw); err == nil { - task.DueDate = &t + // Try multiple date formats (API uses slashes, ISO uses dashes) + formats := []string{ + "2006/01/02", // Checkvist API format + "2006-01-02", // ISO 8601 format + } + for _, format := range formats { + if t, err := time.Parse(format, task.DueDateRaw); err == nil { + task.DueDate = &t + return + } } } diff --git a/tasks_test.go b/tasks_test.go index 40b508d..adbc2d0 100644 --- a/tasks_test.go +++ b/tasks_test.go @@ -73,6 +73,60 @@ func TestTasks_Get(t *testing.T) { } } +func TestTask_ChildIDs_Unmarshal(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1/tasks/101.json": + w.Write(loadFixture(t, "testdata/tasks/single.json")) + case "/checklists/1/tasks.json": + w.Write(loadFixture(t, "testdata/tasks/list.json")) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + + t.Run("single task with child IDs", func(t *testing.T) { + task, err := client.Tasks(1).Get(context.Background(), 101) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedChildIDs := []int{201, 202, 203} + if len(task.ChildIDs) != len(expectedChildIDs) { + t.Fatalf("expected %d child IDs, got %d", len(expectedChildIDs), len(task.ChildIDs)) + } + for i, id := range expectedChildIDs { + if task.ChildIDs[i] != id { + t.Errorf("expected ChildIDs[%d] = %d, got %d", i, id, task.ChildIDs[i]) + } + } + }) + + t.Run("list with mixed child IDs", func(t *testing.T) { + tasks, err := client.Tasks(1).List(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // First task has child IDs [201, 202] + if len(tasks[0].ChildIDs) != 2 { + t.Errorf("expected 2 child IDs for first task, got %d", len(tasks[0].ChildIDs)) + } + if tasks[0].ChildIDs[0] != 201 || tasks[0].ChildIDs[1] != 202 { + t.Errorf("expected ChildIDs [201, 202], got %v", tasks[0].ChildIDs) + } + // Second task has empty child IDs + if len(tasks[1].ChildIDs) != 0 { + t.Errorf("expected 0 child IDs for second task, got %d", len(tasks[1].ChildIDs)) + } + }) +} + func TestTasks_Create(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -141,8 +195,8 @@ func TestTasks_Create_WithBuilder(t *testing.T) { if req.Task.Priority != 1 { t.Errorf("expected priority 1, got %d", req.Task.Priority) } - if req.Task.Due != "^tomorrow" { - t.Errorf("expected due '^tomorrow', got %s", req.Task.Due) + if req.Task.Due != "Tomorrow" { + t.Errorf("expected due 'Tomorrow', got %s", req.Task.Due) } if req.Task.Tags != "tag1, tag2" { t.Errorf("expected tags 'tag1, tag2', got %s", req.Task.Tags) @@ -277,10 +331,11 @@ func TestTasks_Close(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusClosed, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) @@ -310,10 +365,11 @@ func TestTasks_Reopen(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusOpen, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) @@ -343,10 +399,11 @@ func TestTasks_Invalidate(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusInvalidated, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) @@ -376,6 +433,11 @@ func TestDueDate_Parsing(t *testing.T) { dueRaw: "2026-01-20", expected: timePtr(time.Date(2026, 1, 20, 0, 0, 0, 0, time.UTC)), }, + { + name: "Checkvist API format (slashes)", + dueRaw: "2026/01/20", + expected: timePtr(time.Date(2026, 1, 20, 0, 0, 0, 0, time.UTC)), + }, { name: "empty string", dueRaw: "", @@ -413,7 +475,7 @@ func TestTaskBuilder(t *testing.T) { builder := NewTask("Test content"). WithParent(50). WithPosition(3). - WithDueDate(DueNextWeek). + WithDueDate(DueTomorrow). WithPriority(2). WithTags("work", "urgent") @@ -428,8 +490,8 @@ func TestTaskBuilder(t *testing.T) { if req.Position != 3 { t.Errorf("expected Position 3, got %d", req.Position) } - if req.Due != "^Next Monday" { - t.Errorf("expected Due '^Next Monday', got %s", req.Due) + if req.Due != "Tomorrow" { + t.Errorf("expected Due 'Tomorrow', got %s", req.Due) } if req.Priority != 2 { t.Errorf("expected Priority 2, got %d", req.Priority) @@ -480,9 +542,9 @@ func TestTasks_Create_RealAPIFormat(t *testing.T) { ChecklistID: 1, Content: content, Status: StatusOpen, - Priority: 0, // Priority NOT set (ignored) - DueDateRaw: "", // Due date NOT set (ignored) - TagsAsText: "", // Tags NOT set (ignored) + Priority: 0, // Priority NOT set (ignored) + DueDateRaw: "", // Due date NOT set (ignored) + TagsAsText: "", // Tags NOT set (ignored) CreatedAt: NewAPITime(time.Now()), UpdatedAt: NewAPITime(time.Now()), } diff --git a/testdata/tasks/list.json b/testdata/tasks/list.json index b3f5737..50a97fe 100644 --- a/testdata/tasks/list.json +++ b/testdata/tasks/list.json @@ -13,7 +13,8 @@ "comments_count": 0, "update_line": "", "updated_at": "2026/01/14 10:00:00 +0000", - "created_at": "2026/01/10 09:00:00 +0000" + "created_at": "2026/01/10 09:00:00 +0000", + "tasks": [201, 202] }, { "id": 102, @@ -29,6 +30,7 @@ "comments_count": 3, "update_line": "", "updated_at": "2026/01/14 11:00:00 +0000", - "created_at": "2026/01/11 10:00:00 +0000" + "created_at": "2026/01/11 10:00:00 +0000", + "tasks": [] } ] diff --git a/testdata/tasks/single.json b/testdata/tasks/single.json index ebd53a9..1107e24 100644 --- a/testdata/tasks/single.json +++ b/testdata/tasks/single.json @@ -12,5 +12,6 @@ "comments_count": 0, "update_line": "", "updated_at": "2026/01/14 10:00:00 +0000", - "created_at": "2026/01/10 09:00:00 +0000" + "created_at": "2026/01/10 09:00:00 +0000", + "tasks": [201, 202, 203] }