Add GetTimezone() API method and WithTimezone() client option.
When enabled, the client lazily fetches the server timezone on
first API call and converts all Timestamp fields in responses
using reflection-based struct walking.
{"id":"kanboard-1ov","title":"Add GetColorList for available task colors","description":"## Context\n\nTask colors are already partially supported in the library:\n- `ColorID` field exists in the `Task` type\n- `WithColor()` works for task creation via `TaskParams`\n- `SetColor()` works for task updates via `TaskUpdateParams`\n\nHowever, there's no way to query the available colors from the Kanboard instance.\n\n## Missing Feature\n\nThe Kanboard API provides `getColorList` in the [Application Procedures](https://docs.kanboard.org/v1/api/application_procedures/):\n\n```\nRequest:\n{\"jsonrpc\": \"2.0\", \"method\": \"getColorList\", \"id\": 1}\n\nResponse:\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"result\": {\n \"yellow\": \"Yellow\",\n \"blue\": \"Blue\",\n \"green\": \"Green\",\n ...\n }\n}\n```\n\n## Implementation\n\nCreate a new file `application.go` with:\n\n```go\n// GetColorList returns the available task colors.\n// Returns a map of color_id to display name.\nfunc (c *Client) GetColorList(ctx context.Context) (map[string]string, error) {\n var result map[string]string\n if err := c.call(ctx, \"getColorList\", nil, \u0026result); err != nil {\n return nil, fmt.Errorf(\"getColorList: %w\", err)\n }\n return result, nil\n}\n```\n\n## Files to Create/Modify\n\n- `application.go` - New file with `GetColorList()`\n- `application_test.go` - Tests for the new function\n\n## Acceptance Criteria\n\n- [ ] `GetColorList()` returns map of color_id to display name\n- [ ] Works with standard Kanboard color set\n- [ ] Tests written and passing","status":"closed","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T12:11:00.218309278+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T12:13:06.411067375+01:00","closed_at":"2026-01-27T12:13:06.411067375+01:00","close_reason":"Closed"}
{"id":"kanboard-3pe","title":"Add DateMoved timestamp field to Task struct","description":"The Task struct in `types.go` is missing the `date_moved` timestamp that Kanboard returns when a task is moved between columns or swimlanes.\n\n## Background\n\nKanboard tracks when a task was last moved with the `date_moved` field. This timestamp is returned in API responses but is currently not captured by the Go client's Task struct.\n\n## Implementation\n\nAdd the following field to the Task struct in `types.go`:\n\n```go\nDateMoved Timestamp `json:\"date_moved\"`\n```\n\nThis should be added alongside the other date fields (after `DateDue` for consistency with the grouping of timestamp fields).\n\n## Acceptance Criteria\n\n- [ ] `DateMoved` field added to `Task` struct in `types.go`\n- [ ] Field uses `Timestamp` type with JSON tag `json:\"date_moved\"`\n- [ ] All existing tests pass\n- [ ] Field is properly handled in all task-related operations (read/unmarshal)","status":"closed","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T12:05:50.949698791+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T12:09:01.530912406+01:00","closed_at":"2026-01-28T12:09:01.530912406+01:00","close_reason":"Closed"}
{"id":"kanboard-4n3","title":"Implement category write operations","description":"Implement the missing category write operations from the Kanboard API: `createCategory`, `updateCategory`, and `removeCategory`. Additionally, add a client-side `getCategoryByName` helper.\n\nReference: https://docs.kanboard.org/v1/api/category_procedures/\n\n## Details\n\nCurrently only read operations exist (`GetAllCategories`, `GetCategory` in `categories.go`). The following need to be added:\n\n### createCategory\n- Parameters: `project_id` (int), `name` (string), optional `color_id` (string)\n- Returns: category ID on success\n\n### updateCategory\n- Parameters: `id` (int), `name` (string), optional `color_id` (string)\n- Returns: bool\n\n### removeCategory\n- Parameters: `category_id` (int)\n- Returns: bool\n\n### getCategoryByName (client-side helper)\n- Since `getAllCategories` returns the full array, implement `GetCategoryByName(projectID, name)` as a client-side filter over `GetAllCategories` rather than a separate RPC call (unless a server-side procedure exists).\n- Return `ErrCategoryNotFound` if no match.\n\n## Acceptance Criteria\n- [ ] `CreateCategory(projectID, name, opts...)` implemented with functional options for color_id\n- [ ] `UpdateCategory(id, name, opts...)` implemented with functional options for color_id\n- [ ] `RemoveCategory(categoryID)` implemented\n- [ ] `GetCategoryByName(projectID, name)` implemented (client-side filter)\n- [ ] All new methods have corresponding unit tests in `categories_test.go`\n- [ ] BoardScope wrappers added where appropriate\n- [ ] Follow existing code patterns and conventions from the project","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-29T09:12:59.169475651+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-29T09:20:09.394074185+01:00","closed_at":"2026-01-29T09:20:09.394074185+01:00","close_reason":"Closed"}
{"id":"kanboard-521","title":"Add GetTaskByReference function","description":"Add a `GetTaskByReference` function that wraps the Kanboard JSON-RPC `getTaskByReference` endpoint.\n\n**Signature:** `GetTaskByReference(ctx context.Context, projectID int, reference string) (*Task, error)`\n\n- Calls the `getTaskByReference` JSON-RPC method with `project_id` and `reference` parameters\n- Returns the matching `*Task` on success\n- Returns `ErrTaskNotFound` if no task matches the given reference\n\n**Context:** Needed by hqcli for the `find-ref` command (hqcli-a57).\n\n## Acceptance Criteria\n- [ ] `GetTaskByReference` implemented with correct JSON-RPC call\n- [ ] Returns `ErrTaskNotFound` when no task matches\n- [ ] Tests written and passing","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-02T11:22:16.674849553+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-02T11:25:53.151693032+01:00","closed_at":"2026-02-02T11:25:53.151693032+01:00","close_reason":"Closed"}
{"id":"kanboard-7es","title":"JSON-RPC Request-ID: Zufälligen Wert statt fester 1 verwenden","description":"## Kontext\n\nIn jedem JSON-RPC Request an die Kanboard-API wird im Root-Objekt ein Feld `id` mitgeliefert. Dieses dient dazu, bei asynchroner Kommunikation Request und Response einander zuordnen zu können – die API liefert diese ID in der Antwort zurück.\n\n**Aktuell:** Die ID ist fest auf `1` gesetzt.\n\n## Anforderung\n\n1. **Wenn keine ID von außen gesetzt wird:** Die Library soll intern einen zufälligen Wert generieren\n2. **API-Dokumentation prüfen:** Welche Werte sind erlaubt? Welche Größenordnung? (vermutlich Integer)\n3. **Signatur beibehalten:** Die öffentliche API der Library-Funktionen soll unverändert bleiben\n4. **Interne Generierung:** Die Library bestimmt selbst einen zufälligen Wert\n\n## Implementierungshinweise\n\n- Prüfen: Kanboard JSON-RPC Dokumentation bezüglich erlaubter ID-Werte\n- Vermutlich: `int64` oder `int32` Range\n- Zufallsgenerator: `math/rand` mit Seed oder `crypto/rand` für bessere Verteilung\n- Ggf. bestehende `requestIDCounter` in `jsonrpc.go` (Zeile 40) anpassen oder ersetzen\n\n## Beispiel\n\n**Vorher (immer gleich):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 1, \"params\": {...}}\n```\n\n**Nachher (zufällig):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 847291536, \"params\": {...}}\n```\n\n## Referenz\n\n- Datei: `jsonrpc.go`\n- Zeile 17: `ID int64 \\`json:\"id\"\\``\n- Zeile 40: `requestIDCounter` (existiert bereits)","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:44:51.566737509+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:21:06.415129879+01:00","closed_at":"2026-01-27T10:21:06.415129879+01:00","close_reason":"Closed"}
{"id":"kanboard-9wa","title":"Support custom authentication header name","description":"## Description\n\nKanboard supports using an alternative HTTP header for authentication when the server has specific configuration requirements.\n\nCurrently, authentication uses the standard `Authorization` header via Go's `SetBasicAuth()`. This needs to be configurable so users can specify a custom header name (e.g., `X-API-Auth`).\n\n## Requirements\n\n- Add an optional configuration parameter for custom auth header name\n- Default to standard `Authorization` header if not specified\n- When custom header is set, use that header name instead of `Authorization`\n- The header value format should remain the same (Basic Auth base64-encoded credentials)\n\n## Acceptance Criteria\n\n- [ ] New client configuration method (e.g., `WithAuthHeader(headerName string)`)\n- [ ] Default behavior unchanged when no custom header specified\n- [ ] Custom header name is used when configured\n- [ ] Works with both API token and basic auth\n- [ ] Tests cover default and custom header scenarios\n\n## Reference\n\nKanboard API documentation: \"You can use an alternative HTTP header for authentication if your server has a very specific configuration.\"","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T18:08:31.507616093+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T18:26:50.40804952+01:00","closed_at":"2026-01-23T18:26:50.40804952+01:00","close_reason":"Closed"}
{"id":"kanboard-a7x","title":"Handle URLs already ending in /jsonrpc.php","description":"## Context\n\nThe `NewClient()` function in `client.go` always appends `/jsonrpc.php` to the base URL. This causes issues when users pass a URL that already includes the endpoint path.\n\n## Problem\n\nIf a user calls:\n```go\nclient := kanboard.NewClient(\"https://example.com/jsonrpc.php\")\n```\n\nThe resulting endpoint becomes `https://example.com/jsonrpc.php/jsonrpc.php`, which fails.\n\n## Solution\n\nModify `NewClient()` to detect and handle URLs that already end in `/jsonrpc.php`:\n\n```go\nfunc NewClient(baseURL string) *Client {\n // Ensure no trailing slash\n baseURL = strings.TrimSuffix(baseURL, \"/\")\n\n // Handle URLs that already include /jsonrpc.php\n endpoint := baseURL\n if !strings.HasSuffix(baseURL, \"/jsonrpc.php\") {\n endpoint = baseURL + \"/jsonrpc.php\"\n }\n\n c := \u0026Client{\n baseURL: baseURL,\n endpoint: endpoint,\n }\n // ... rest unchanged\n}\n```\n\n## Files to Modify\n\n- `client.go` - Update `NewClient()` to check for existing `/jsonrpc.php` suffix\n\n## Acceptance Criteria\n\n- [ ] `NewClient(\"https://example.com\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/jsonrpc.php\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/kanboard/jsonrpc.php\")` → endpoint `https://example.com/kanboard/jsonrpc.php`\n- [ ] Tests written and passing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:25:51.077352962+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:27:15.189568683+01:00","closed_at":"2026-01-27T10:27:15.189568683+01:00","close_reason":"Closed"}
@ -34,4 +35,6 @@
{"id":"kanboard-api-zfc","title":"Implement TaskScope tag methods with read-modify-write (CRITICAL)","description":"Implement TaskScope tag methods with read-modify-write pattern. CRITICAL feature.\n\n## TaskScope methods to implement\n- GetTags(ctx) (map[int]string, error)\n- SetTags(ctx, tags ...string) error - replaces ALL tags\n- ClearTags(ctx) error - removes ALL tags\n- AddTag(ctx, tag string) error - read-modify-write\n- RemoveTag(ctx, tag string) error - read-modify-write\n- HasTag(ctx, tag string) (bool, error)\n\n## Read-Modify-Write Workflow for AddTag\n1. Get task via getTask (need project_id)\n2. Get current tags via getTaskTags\n3. Check if tag already exists\n4. If not: add tag to list\n5. Call setTaskTags with updated list\n\n## Read-Modify-Write Workflow for RemoveTag\n1. Get task via getTask (need project_id)\n2. Get current tags via getTaskTags\n3. Filter out the tag to remove\n4. Call setTaskTags with filtered list\n5. If tag didn't exist: no error (idempotent)\n\n## Files to modify\n- task_scope.go\n\n## IMPORTANT WARNING\nThis is NOT atomic. Concurrent tag modifications may cause data loss. Document this limitation.\n\n## Acceptance criteria\n- AddTag is idempotent (no error if tag exists)\n- RemoveTag is idempotent (no error if tag doesn't exist)\n- HasTag correctly checks tag existence","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.911429864+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:36:32.533937315+01:00","closed_at":"2026-01-15T18:36:32.533937315+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-16r","type":"blocks","created_at":"2026-01-15T17:43:49.517064988+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-una","type":"blocks","created_at":"2026-01-15T17:43:49.593313748+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zfc","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:49.690872321+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-zg2","title":"Implement File API methods","description":"Implement direct API methods for task file operations.\n\n## Methods to implement (Must-have)\n- GetAllTaskFiles(ctx, taskID int) ([]TaskFile, error) - getAllTaskFiles\n- CreateTaskFile(ctx, projectID, taskID int, filename string, content []byte) (int, error) - createTaskFile\n\n## Methods to implement (Nice-to-have)\n- DownloadTaskFile(ctx, fileID int) ([]byte, error) - downloadTaskFile\n- RemoveTaskFile(ctx, fileID int) error - removeTaskFile\n\n## TaskScope methods to add\n- GetFiles(ctx) ([]TaskFile, error)\n- UploadFile(ctx, filename string, content []byte) (*TaskFile, error)\n\n## Files to create\n- files.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- File content base64 encoded for upload\n- CreateTaskFile returns file ID","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.748005313+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:45:05.499871045+01:00","closed_at":"2026-01-15T18:45:05.499871045+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-zg2","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.984099418+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-zg2","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:50.049331328+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-iuf","title":"Remove Basic prefix from custom auth headers","description":"## Problem\n\nWhen using a custom header name (e.g., `X-API-Auth` for Kanboard), the library incorrectly prepends `\"Basic \"` to the base64-encoded credentials.\n\n**Current behavior** (`auth.go:27` and `auth.go:43`):\n```go\nreq.Header.Set(a.headerName, \"Basic \"+basicAuthValue(user, a.token))\n```\n\nSends: `X-API-Auth: Basic dXNlcjpwYXNzd29yZA==`\n\n**Expected behavior** per [Kanboard API docs](https://docs.kanboard.org/v1/api/authentication/):\n\n- Standard `Authorization` header: `Basic base64(username:password)` ✓\n- Custom `X-API-Auth` header: `base64(username:password)` (no \"Basic \" prefix!)\n\nShould send: `X-API-Auth: dXNlcjpwYXNzd29yZA==`\n\n## Impact\n\nAuthentication fails when using the custom header option. The server returns HTML (login page) instead of JSON because authentication is rejected.\n\n## Suggested Fix\n\nIn both `apiTokenAuth.Apply()` and `basicAuth.Apply()`, remove the \"Basic \" prefix when using a custom header name:\n\n```go\nif a.headerName != \"\" {\n req.Header.Set(a.headerName, basicAuthValue(user, a.token)) // no \"Basic \" prefix\n} else {\n req.SetBasicAuth(user, a.token) // uses standard Authorization header with \"Basic \"\n}\n```\n\n## Affected Code\n\n- `auth.go:27` - `apiTokenAuth.Apply()`\n- `auth.go:43` - `basicAuth.Apply()`\n\n## Acceptance Criteria\n\n- [ ] Custom header (`X-API-Auth`) sends raw base64 value without \"Basic \" prefix\n- [ ] Standard `Authorization` header still works with \"Basic \" prefix\n- [ ] Tests updated to verify both behaviors\n- [ ] All tests passing","status":"closed","priority":1,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:43:21.717917986+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T11:01:19.467981335+01:00","closed_at":"2026-01-27T11:01:19.467981335+01:00","close_reason":"Closed"}
{"id":"kanboard-n03","title":"Implement Swimlane CRUD methods","description":"The kanboard-api client already supports swimlane_id on tasks (InSwimlane builder, MoveTaskPosition, Task.SwimlaneID field) but lacks dedicated Swimlane management methods.\n\nImplement the following Kanboard JSON-RPC API methods:\n\n**Read operations:**\n- getActiveSwimlanes(project_id)\n- getAllSwimlanes(project_id)\n- getSwimlane(swimlane_id)\n- getSwimlaneById(swimlane_id)\n- getSwimlaneByName(project_id, name)\n\n**Write operations:**\n- addSwimlane(project_id, name, description)\n- updateSwimlane(swimlane_id, name, description)\n- removeSwimlane(project_id, swimlane_id)\n- changeSwimlanePosition(project_id, swimlane_id, position)\n- enableSwimlane(project_id, swimlane_id)\n- disableSwimlane(project_id, swimlane_id)\n\nCreate a new file `swimlanes.go` with a Swimlane struct:\n```go\ntype Swimlane struct {\n ID int\n Name string\n Description string\n Position int\n IsActive bool\n ProjectID int\n}\n```\n\nAnd corresponding test file `swimlanes_test.go`.\n\n## Acceptance Criteria\n- [ ] Swimlane struct defined with ID, Name, Description, Position, IsActive, ProjectID\n- [ ] All 11 JSON-RPC methods implemented\n- [ ] Read methods: getActiveSwimlanes, getAllSwimlanes, getSwimlane, getSwimlaneById, getSwimlaneByName\n- [ ] Write methods: addSwimlane, updateSwimlane, removeSwimlane, changeSwimlanePosition, enableSwimlane, disableSwimlane\n- [ ] Code in swimlanes.go follows existing patterns (e.g., categories.go)\n- [ ] Tests written in swimlanes_test.go and passing","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-01T10:07:54.192295081+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-01T10:07:54.192295081+01:00"}
{"id":"kanboard-qle","title":"Add timezone support with automatic timestamp conversion","description":"## Description\n\nImplement timezone support by calling the Kanboard `getTimezone` API endpoint and converting all UTC timestamps to the configured timezone.\n\nThe Kanboard API provides `getTimezone` which returns a timezone string (e.g. `Europe/Berlin`). The client should:\n\n1. Query the timezone on initialization (or lazily on first use)\n2. Automatically convert all timestamp fields to local time, including:\n - `date_creation`, `date_started`, `date_moved`, `date_due`\n - Comment dates (`date_creation`)\n - Any other timestamp fields returned by the API\n\n### Design Option\n\nProvide a `WithTimezone()` client option so callers can control whether conversion happens. When enabled, the client fetches the timezone from the API and converts timestamps transparently. When disabled (default for backward compatibility), timestamps remain as-is.\n\n## Acceptance Criteria\n\n- [ ] Implement `GetTimezone()` API method that calls `getTimezone`\n- [ ] Add `WithTimezone()` client option to enable automatic conversion\n- [ ] When enabled, all timestamp fields in responses are converted to the configured timezone\n- [ ] Timezone is fetched once and cached for the client lifetime\n- [ ] Tests written and passing for timezone conversion logic\n- [ ] Backward compatible: no behavior change without `WithTimezone()` option","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-02T12:25:32.830875466+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-02T12:34:10.955810869+01:00","closed_at":"2026-02-02T12:34:10.955810869+01:00","close_reason":"Closed"}
{"id":"kanboard-r3p","title":"Support optional API user in token authentication","description":"## Description\n\nBei der API-Token-Authentifizierung soll neben dem API-Key optional auch ein API-User konfigurierbar sein. Wenn kein User angegeben wird, soll weiterhin der Standard-User \"jsonrpc\" verwendet werden.\n\n## Current Behavior\n\nIn `auth.go`, the `apiTokenAuth` struct hardcodes the username \"jsonrpc\":\n\n```go\nfunc (a *apiTokenAuth) Apply(req *http.Request) {\n req.SetBasicAuth(\"jsonrpc\", a.token)\n}\n```\n\n## Expected Behavior\n\n- Add an optional `user` field to `apiTokenAuth`\n- If user is empty/not provided, default to \"jsonrpc\"\n- If user is provided, use that value for HTTP Basic Auth\n\n## Acceptance Criteria\n\n- [ ] `apiTokenAuth` struct has an optional user field\n- [ ] Default behavior unchanged when no user specified (uses \"jsonrpc\")\n- [ ] Custom user is used when explicitly provided\n- [ ] Client configuration supports setting the API user\n- [ ] Tests cover both default and custom user scenarios","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:39:37.745294723+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T17:55:23.860864584+01:00","closed_at":"2026-01-23T17:55:23.860864584+01:00","close_reason":"Closed"}