From c15d52633fbb5345415a82394227b274116f5f46 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Tue, 27 Jan 2026 12:13:18 +0100 Subject: [PATCH] feat: add GetColorList for available task colors Adds GetColorList() to query the available task colors from the Kanboard instance. Returns a map of color_id to display name (e.g., "yellow" -> "Yellow"). Co-Authored-By: Claude Opus 4.5 --- .beads/export-state/fbbdf412d0fd5173.json | 6 +- .beads/issues.jsonl | 1 + application.go | 17 ++++ application_test.go | 94 +++++++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 application.go create mode 100644 application_test.go diff --git a/.beads/export-state/fbbdf412d0fd5173.json b/.beads/export-state/fbbdf412d0fd5173.json index 832f473..80ee16a 100644 --- a/.beads/export-state/fbbdf412d0fd5173.json +++ b/.beads/export-state/fbbdf412d0fd5173.json @@ -1,6 +1,6 @@ { "worktree_root": "/home/oli/Dev/kanboard-api", - "last_export_commit": "508c3ac6d2f3cd76cdbd540b724e8efb6c74bd0c", - "last_export_time": "2026-01-27T11:12:10.369070811+01:00", - "jsonl_hash": "a6d0e597873b206d5a2394753518ab6bcf07158f415728c695d08083c5e39d0b" + "last_export_commit": "449cd2626c2990199ccad4b08041c0e438858272", + "last_export_time": "2026-01-27T12:13:18.925035335+01:00", + "jsonl_hash": "a60a9c8ba481e1eb8a3879ae02ed3fadda20082c4f0a618fe05f2c1c58b02e43" } \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 85cbc6b..e0bb016 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,5 @@ {"id":"kanboard-1cb","title":"Investigate MoveTaskPosition generic error message","description":"## Context\n\nThis is an investigation task to determine if a problem in the `hqcli` tool (which uses this library) has its root cause here.\n\nThe `kb ticket move` command in hqcli fails with an unhelpful error:\n\n```\n❯ dist/hqcli kb ticket move 3765\n2026/01/27 11:02:43 Fehler beim Verschieben des Tickets: moveTaskPosition: failed to move task 3765\n```\n\n## Library Behavior\n\nLooking at `tasks.go:131-150`, the `MoveTaskPosition` function:\n\n```go\nfunc (c *Client) MoveTaskPosition(...) error {\n // ...\n var success bool\n if err := c.call(ctx, \"moveTaskPosition\", params, \u0026success); err != nil {\n return fmt.Errorf(\"moveTaskPosition: %w\", err)\n }\n\n if !success {\n return fmt.Errorf(\"moveTaskPosition: failed to move task %d\", taskID) // \u003c-- generic error\n }\n return nil\n}\n```\n\nWhen the API returns `false`, the error message is completely generic with no actionable information.\n\n## Investigation Needed\n\n1. **API Response Analysis**: What does Kanboard actually return when `moveTaskPosition` fails?\n - Does it include an error message in the JSON-RPC response?\n - Is the `false` result the only indication of failure?\n\n2. **Possible Causes for API returning false**:\n - Invalid column/project combination\n - Permission issues\n - Task doesn't exist\n - Position 0 might have different semantics than documented\n\n3. **Documentation Check**: [Kanboard API docs](https://docs.kanboard.org/v1/api/task_procedures/#movetaskposition)\n\n## Potential Improvements\n\nIf the API provides no additional details, consider:\n- Adding debug logging to show the full API response\n- Checking if the task/column/project exist before calling\n- Providing a more descriptive error: \"moveTaskPosition returned false - verify task exists, column belongs to project, and user has permission\"\n\n## Acceptance Criteria\n\n- [ ] Root cause identified (is it library issue or hqcli usage issue?)\n- [ ] If library issue: improve error message or error handling\n- [ ] Document findings","status":"closed","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T11:05:16.089428488+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T11:11:58.26724838+01:00","closed_at":"2026-01-27T11:11:58.26724838+01:00","close_reason":"Closed"} +{"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-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"} diff --git a/application.go b/application.go new file mode 100644 index 0000000..5ed8ffb --- /dev/null +++ b/application.go @@ -0,0 +1,17 @@ +package kanboard + +import ( + "context" + "fmt" +) + +// GetColorList returns the available task colors. +// Returns a map of color_id to display name (e.g., "yellow" -> "Yellow"). +func (c *Client) GetColorList(ctx context.Context) (map[string]string, error) { + var result map[string]string + if err := c.call(ctx, "getColorList", nil, &result); err != nil { + return nil, fmt.Errorf("getColorList: %w", err) + } + + return result, nil +} diff --git a/application_test.go b/application_test.go new file mode 100644 index 0000000..78eaf0c --- /dev/null +++ b/application_test.go @@ -0,0 +1,94 @@ +package kanboard + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestClient_GetColorList(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "getColorList" { + t.Errorf("expected method=getColorList, got %s", req.Method) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`{ + "yellow": "Yellow", + "blue": "Blue", + "green": "Green", + "purple": "Purple", + "red": "Red", + "orange": "Orange", + "grey": "Grey", + "brown": "Brown", + "deep_orange": "Deep Orange", + "dark_grey": "Dark Grey", + "pink": "Pink", + "teal": "Teal", + "cyan": "Cyan", + "lime": "Lime", + "light_green": "Light Green", + "amber": "Amber" + }`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + colors, err := client.GetColorList(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check that we got some colors + if len(colors) != 16 { + t.Errorf("expected 16 colors, got %d", len(colors)) + } + + // Check specific colors + if colors["yellow"] != "Yellow" { + t.Errorf("expected yellow='Yellow', got %s", colors["yellow"]) + } + if colors["blue"] != "Blue" { + t.Errorf("expected blue='Blue', got %s", colors["blue"]) + } + if colors["deep_orange"] != "Deep Orange" { + t.Errorf("expected deep_orange='Deep Orange', got %s", colors["deep_orange"]) + } +} + +func TestClient_GetColorList_Empty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`{}`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + colors, err := client.GetColorList(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(colors) != 0 { + t.Errorf("expected 0 colors, got %d", len(colors)) + } +}